<!-- templates/trainer_htmx.html - Enhanced trainer page with HTMX + Alpine.js -->
{% extends "base_htmx.html" %}

{% block title %}SimpleTuner Training Studio{% endblock %}

{% block extra_css %}
<!-- Load design tokens first -->
<link href="/static/css/tokens.css" rel="stylesheet" id="css-tokens">
<script>document.getElementById('css-tokens').href = '/static/css/tokens.css?t=' + cssTimestamp;</script>
<!-- Load existing trainer CSS for consistency -->
<link href="/static/css/trainer.css" rel="stylesheet" id="css-trainer-extra">
<script>document.getElementById('css-trainer-extra').href = '/static/css/trainer.css?t=' + cssTimestamp;</script>
<link href="/static/css/dataloader-builder.css" rel="stylesheet" id="css-dataloader-builder">
<script>document.getElementById('css-dataloader-builder').href = '/static/css/dataloader-builder.css?t=' + cssTimestamp;</script>
<link href="/static/css/trainer-core.css" rel="stylesheet" id="css-trainer-core">
<script>document.getElementById('css-trainer-core').href = '/static/css/trainer-core.css?t=' + cssTimestamp;</script>
<link href="/static/css/environments.css" rel="stylesheet" id="css-environments">
<script>document.getElementById('css-environments').href = '/static/css/environments.css?t=' + cssTimestamp;</script>
<link href="/static/css/tab-navigation.css" rel="stylesheet" id="css-tab-navigation">
<script>document.getElementById('css-tab-navigation').href = '/static/css/tab-navigation.css?t=' + cssTimestamp;</script>
<!-- HTMX loading states -->
<link href="/static/css/htmx-loading-states.css" rel="stylesheet" id="css-htmx-loading-states">
<script>document.getElementById('css-htmx-loading-states').href = '/static/css/htmx-loading-states.css?t=' + cssTimestamp;</script>
<!-- Event lightbox -->
<link href="/static/css/event_lightbox.css" rel="stylesheet" id="css-event-lightbox">
<script>document.getElementById('css-event-lightbox').href = '/static/css/event_lightbox.css?t=' + cssTimestamp;</script>
<!-- Streaming validation lightbox -->
<link href="/static/css/streaming-validation-lightbox.css" rel="stylesheet" id="css-streaming-validation-lightbox">
<script>document.getElementById('css-streaming-validation-lightbox').href = '/static/css/streaming-validation-lightbox.css?t=' + cssTimestamp;</script>
{% endblock %}

{% block body %}
<script>
// Initialize Alpine store early to prevent race conditions
document.addEventListener('alpine:init', () => {
    Alpine.store('trainer', {
        formDirty: false,
        showConfigSaveDialog: false,
        createConfigBackupOption: false,
        preserveDefaultsOption: false,
        saveConfig: () => {
            console.warn('Trainer component not fully initialized yet');
            window.showToast('Please wait for the page to fully load', 'warning');
        },
        cancelSaveConfig: () => {},
        confirmSaveConfig: () => {}
    });

    // Shared dataloader configs store
    Alpine.store('dataloaderConfigs', {
        configs: [],
        loaded: false,
        loading: false,

        async load(force = false) {
            if (this.loading) return;
            if (this.loaded && !force) return;

            this.loading = true;
            try {
                const response = await fetch('/api/configs/?config_type=dataloader');
                if (response.ok) {
                    const data = await response.json();
                    this.configs = (data.configs || []).map(item => ({
                        name: item.name,
                        path: item.path || '',
                        dataset_count: item.dataset_count || 0,
                        absolute_path: item.absolute_path || item.path || ''
                    })).filter(item => item.path);
                    this.loaded = true;
                }
            } catch (error) {
                console.error('Failed to load dataloader configs:', error);
            } finally {
                this.loading = false;
            }
        },

        getByPath(path) {
            return this.configs.find(c => c.path === path);
        },

        filterByEnvironment(environmentName) {
            if (!environmentName) return this.configs;
            return this.configs.filter(config => {
                const path = config.path || '';
                const name = config.name || '';
                return path.startsWith(environmentName + '/') || name.startsWith(environmentName);
            });
        }
    });
});

const sanitizeConfigName = (name) => {
    if (typeof name !== 'string') {
        return name;
    }
    const trimmed = name.trim();
    return trimmed.toLowerCase().endsWith('.json') ? trimmed.slice(0, -5) : trimmed;
};

const sanitizeConfigEntry = (entry) => {
    if (!entry || typeof entry !== 'object') {
        return entry;
    }
    const clone = { ...entry };
    if (clone.name) {
        clone.name = sanitizeConfigName(clone.name);
    }
    return clone;
};

window.sanitizeConfigName = sanitizeConfigName;
window.sanitizeConfigEntry = sanitizeConfigEntry;

function trainerComponent() {
    return {
        versionInfo: null,
        _versionLoading: false,
        handleTrainingStatus(e) {
            const detail = e.detail || {};
            const status = (detail.status || '').toLowerCase();
            const isTraining = ['running', 'starting', 'initializing'].includes(status);

            // Don't reset isTraining to false if we just got a disconnected event but have active startup stages
            // This prevents the UI from flashing to "idle" during a quick reconnect
            if (status === 'disconnected' && this.isTraining) {
                return;
            }

            this.isTraining = isTraining;
            if (!isTraining) {
                this.showTrainingProgress = false;
            }
        },
        async loadVersionInfo(force = false) {
            if (this._versionLoading) {
                return;
            }
            if (!force && this.versionInfo && this.versionInfo.version) {
                return;
            }
            this._versionLoading = true;
            try {
                const response = await fetch('/api/version', { cache: 'no-store' });
                if (!response.ok) {
                    throw new Error(`Request failed with status ${response.status}`);
                }
                const payload = await response.json();
                const rawMajor = payload?.major;
                let major = null;
                if (typeof rawMajor === 'number' && Number.isFinite(rawMajor)) {
                    major = rawMajor;
                } else if (typeof rawMajor === 'string') {
                    const parsed = parseInt(rawMajor, 10);
                    if (!Number.isNaN(parsed)) {
                        major = parsed;
                    }
                }
                const dirtyFlag = payload?.git_dirty;
                const gitDirty =
                    typeof dirtyFlag === 'boolean'
                        ? dirtyFlag
                        : null;
                const commit = typeof payload?.git_commit === 'string' && payload.git_commit.trim()
                    ? payload.git_commit.trim()
                    : null;
                this.versionInfo = {
                    version: payload?.version ?? null,
                    major,
                    git_install: Boolean(payload?.git_install),
                    git_commit: commit,
                    git_dirty: gitDirty,
                };
            } catch (error) {
                console.debug('Failed to load version info:', error);
            } finally {
                this._versionLoading = false;
            }
        },
        sidebarVersionText() {
            const info = this.versionInfo;
            if (!info) {
                return '';
            }
            const parts = [];
            if (info.version) {
                parts.push(`v${String(info.version)}`);
            } else if (typeof info.major === 'number' && Number.isFinite(info.major)) {
                parts.push(`v${info.major}.x`);
            }
            if (info.git_install) {
                const commitLabel = info.git_commit || 'repo';
                const dirtySuffix = info.git_dirty ? '*' : '';
                parts.push(`git ${commitLabel}${dirtySuffix}`);
            }
            return parts.join(' • ');
        },
        init() {
            this.loadVersionInfo().catch((error) => {
                console.debug('Version info fetch failed:', error);
            });
            const handler = (event) => {
                const payload = (event && event.detail && (event.detail.resolved_defaults || event.detail.defaults)) || null;
                if (!payload) {
                    return;
                }
                const normalizedDefaults = this.normalizeDefaultsPayload(payload);
                this.defaults = normalizedDefaults;
                this.preserveDefaultsOption = normalizedDefaults.auto_preserve_defaults;
                if (typeof window.applyTheme === 'function' && this.defaults.theme) {
                    window.applyTheme(this.defaults.theme);
                }
                this.applyEventPreferences();
            };
            this._defaultsUpdateHandler = handler;
            window.addEventListener('webui-defaults-updated', handler);
            const beforeUnloadHandler = () => {
                if (this._defaultsUpdateHandler) {
                    window.removeEventListener('webui-defaults-updated', this._defaultsUpdateHandler);
                }
                if (this._systemStatusTimer) {
                    window.clearInterval(this._systemStatusTimer);
                    this._systemStatusTimer = null;
                }
                if (this._visibilityChangeHandler) {
                    document.removeEventListener('visibilitychange', this._visibilityChangeHandler);
                    this._visibilityChangeHandler = null;
                }
            };
            this._beforeUnloadHandler = beforeUnloadHandler;
            window.addEventListener('beforeunload', beforeUnloadHandler);

            this.refreshSystemStatus();
            this._systemStatusTimer = window.setInterval(() => {
                if (this.isDocumentVisible()) {
                    this.refreshSystemStatus();
                }
            }, 10000);

            const visibilityHandler = () => {
                if (this.isDocumentVisible()) {
                    this.refreshSystemStatus();
                    if (!this._systemStatusTimer) {
                        this._systemStatusTimer = window.setInterval(() => {
                            if (this.isDocumentVisible()) {
                                this.refreshSystemStatus();
                            }
                        }, 10000);
                    }
                } else if (this._systemStatusTimer) {
                    window.clearInterval(this._systemStatusTimer);
                    this._systemStatusTimer = null;
                }
            };
            this._visibilityChangeHandler = visibilityHandler;
            document.addEventListener('visibilitychange', visibilityHandler);
        },
        autoPreserveEnabled(value) {
            if (value === undefined || value === null) {
                return true;
            }
            if (typeof value === 'boolean') {
                return value;
            }
            if (typeof value === 'string') {
                const normalized = value.trim().toLowerCase();
                if (!normalized) {
                    return true;
                }
                return !['0', 'false', 'no', 'off'].includes(normalized);
            }
            if (typeof value === 'number' && Number.isFinite(value)) {
                return value !== 0;
            }
            return Boolean(value);
        },
        normalizeDefaultsPayload(payload) {
            const data = payload || {};
            const preserveFlag = this.autoPreserveEnabled(data.auto_preserve_defaults);
            return {
                ...data,
                auto_preserve_defaults: preserveFlag,
            };
        },
         // Tab management
         activeTab: window.location.hash.slice(1) || localStorage.getItem('activeTab') || 'basic',

        switchTab(tabName, updateHash = true) {
            console.log('Alpine store switchTab called with:', tabName);

            // Save current form values before switching tabs
            // Access the trainer component through the Alpine store
            const trainerStore = window.Alpine && window.Alpine.store ? window.Alpine.store('trainer') : null;
            if (trainerStore && typeof trainerStore.captureFormValues === 'function') {
                console.log('Saving form values before tab switch');
                trainerStore.captureFormValues();
                // Set flag to restore form values after tab loads
                trainerStore._preservingFormState = true;
            }

            this.activeTab = tabName;
            localStorage.setItem('activeTab', tabName);

            // Update URL if needed
            if (updateHash && window.location.hash.slice(1) !== tabName) {
                history.replaceState(null, '', '#' + tabName);
            }

            // Ensure tab button gets active class
            this.$nextTick(() => {
                const allTabButtons = document.querySelectorAll('.tab-btn');
                allTabButtons.forEach(button => {
                    button.classList.remove('active');
                    if (button.getAttribute('data-tab') === tabName) {
                        button.classList.add('active');
                    }
                });
            });
        },
        activateTab(tabName, updateHash = true) {
            const tabButton = document.querySelector(`.tab-btn[data-tab="${tabName}"]`);
            if (tabButton) {
                tabButton.click();
                this.switchTab(tabName, updateHash);
            }
        },
        async updateConfigSelectors() {
             // Update all config selector instances with current active config
             const configSelectors = document.querySelectorAll('[x-data="configSelectorComponent()"]');
             const updatePromises = [];
             for (const selector of configSelectors) {
                 const component = selector.__x?.$data;
                 if (!component) {
                     continue;
                 }
                 component.activeConfig = this.activeEnvironment;
                 if (typeof component.loadConfigs === 'function') {
                     updatePromises.push(
                         Promise.resolve(component.loadConfigs()).catch((error) => {
                             console.error('Failed to refresh config selector:', error);
                         })
                     );
                 }
             }
             if (updatePromises.length) {
                 await Promise.allSettled(updatePromises);
             }
         },

         // Form and training state
         formData: {},
         validationErrors: {},
         isTraining: false,
         showTrainingProgress: false,
        trainingProgress: {},
        connectionStatus: 'connected',
        defaults: null,
        mergeEnvironmentDefaults: (() => {
            try {
                const stored = localStorage.getItem('simpletuner.mergeEnvironmentConfig');
                if (stored === 'true' || stored === 'false') {
                    return stored === 'true';
                }
            } catch (err) {
                console.debug('Unable to read mergeEnvironmentConfig preference', err);
            }
            return false;
        })(),
        activeModelFamily: null,
        activeModelType: null,
        activeModelFlavour: null,
        activeLoraType: null,
        modelDetails: null,
        modelContext: {
            requiresConditioningDataset: false,
            requiresConditioningLatents: false,
            requiresConditioningImageEmbeds: false,
            requiresConditioningValidationInputs: false,
            requiresValidationEditCaptions: false,
            requiresValidationI2VSamples: false,
            requiresStrictI2VDatasets: false,
            strictI2VFlavours: [],
            supportsConditioningGenerators: false,
            hasControlnetPipeline: false,
            modelFlavour: null,
            controlnetEnabled: false,
            controlEnabled: false,
            isVideoModel: false,
            supportsVideo: false,
            conditioningDatasetType: 'conditioning',
        },
        activeEnvironmentConfig: null,
        activeEnvironmentMetadata: null,
        configValues: {},
        showConfigJsonModal: false,
        configJsonDraft: '',
        configJsonError: '',
        _modelDetailsCache: {},
         overlayVisible: false,
         overlaySaving: false,
         overlayError: '',
         onboardingSteps: [],
         activeOnboardingStep: null,
         onboardingForm: { value: '' },
         gpuInventory: {
             loading: false,
             error: '',
             devices: [],
             backend: null,
             count: 0,
             detected: false,
             optimalProcesses: 1,
         },
         onboardingAccelerate: {
             mode: 'auto',
             manualCount: 1,
             deviceIds: [],
         },

        // Form dirty tracking
        formDirty: false,
        _skipNextClean: false,
        originalFormData: {},
        formValueStore: {},

         // Config save dialog state
         showConfigSaveDialog: false,
         createConfigBackupOption: false,
         preserveDefaultsOption: false,
         configSaveDialogResolve: null,
         // Environment config management
         environments: [],
         activeEnvironment: null,
         environmentsLoading: false,
        async loadEnvironmentConfigs() {
            this.environmentsLoading = true;
            try {
                const response = await fetch('/api/configs/');
                if (!response.ok) {
                     console.error('Failed to load environment configs');
                     window.showToast('Failed to load environment configurations', 'error');
                     return;
                }
                const data = await response.json();
                const configs = Array.isArray(data.configs) ? data.configs.map(sanitizeConfigEntry) : [];
                const active = sanitizeConfigName(data.active || null);
                this.environments = configs;
                this.activeEnvironment = active || (configs.length > 0 ? configs[0].name : null);
                await this.fetchActiveEnvironmentConfig();
            } catch (error) {
                console.error('Error loading configs:', error);
                window.showToast('Error loading environment configurations', 'error');
            } finally {
                 this.environmentsLoading = false;
             }
         },
        async switchEnvironment(configName) {
            try {
                const sanitizedName = sanitizeConfigName(configName);
                const response = await fetch(`/api/configs/${encodeURIComponent(sanitizedName)}/activate`, {
                    method: 'POST'
                });
                if (!response.ok) {
                    throw new Error('Failed to activate configuration');
                }
                const previousEnvironment = this.activeEnvironment;
                this.activeEnvironment = sanitizedName;

                 // Update the config selector component
                 // Need to find all config selector instances (might be multiple after tab reloads)
                const configSelectors = document.querySelectorAll('[x-data="configSelectorComponent()"]');
                for (const selector of configSelectors) {
                    if (selector.__x) {
                        selector.__x.$data.activeConfig = sanitizedName;
                        await selector.__x.$data.loadConfigs();
                        await selector.__x.$data.validateActiveConfig();
                    }
                }

                // Reset stored form state so tabs reload with fresh values
                this.formValueStore = {};
                this.originalFormData = {};
                this.formDirty = false;
                this._skipNextClean = false;
                delete this._preservingFormState;

                // Broadcast environment change event for other components
                window.dispatchEvent(new CustomEvent('environment-changed', {
                    detail: {
                        environment: sanitizedName,
                        previousEnvironment: previousEnvironment
                     }
                 }));

                await this.fetchActiveEnvironmentConfig();
                await this.loadDatasetsAfterEnvironmentChange();

                 window.showToast(`Switched to ${configName} configuration`, 'success');
                 // Refresh current tab content instead of reloading
                 await this.refreshAfterEnvironmentSwitch();
             } catch (error) {
                 console.error('Error switching config:', error);
                 window.showToast('Failed to switch configuration', 'error');
             }
         },
        async refreshAfterEnvironmentSwitch() {
            // Store the current active tab to ensure it doesn't reset
            const currentTab = this.activeTab;

            // Refresh current tab content
            if (this.activeTab === 'datasets') {
                await this.loadDatasetsAfterEnvironmentChange();
            } else if (this.activeTab !== 'environments') {
                // For other tabs (except environments), reload the tab content to show new config values
                const tabContainer = document.getElementById('tab-content');
                if (tabContainer) {
                    tabContainer.innerHTML = `
                        <div class="text-center py-5">
                            <i class="fas fa-spinner fa-spin fa-2x"></i>
                            <p class="mt-3">Loading configuration...</p>
                        </div>`;
                    try {
                        await new Promise((resolve, reject) => {
                            const targetId = 'tab-content';

                            const cleanup = () => {
                                document.body.removeEventListener('htmx:afterSwap', handleSwap);
                                document.body.removeEventListener('htmx:responseError', handleError);
                                document.body.removeEventListener('htmx:sendError', handleError);
                            };

                            const handleSwap = (evt) => {
                                if (evt.detail && evt.detail.target && evt.detail.target.id === targetId) {
                                    cleanup();
                                    resolve();
                                }
                            };

                            const handleError = (evt) => {
                                if (evt.detail && evt.detail.target && evt.detail.target.id === targetId) {
                                    cleanup();
                                    reject(new Error('HTMX request failed'));
                                }
                            };

                            document.body.addEventListener('htmx:afterSwap', handleSwap);
                            document.body.addEventListener('htmx:responseError', handleError);
                            document.body.addEventListener('htmx:sendError', handleError);

                            const request = htmx.ajax('GET', `/web/trainer/tabs/${currentTab}`, {
                                target: `#${targetId}`,
                                swap: 'innerHTML'
                            });

                            if (!request) {
                                cleanup();
                                resolve();
                            }
                        });
                    } catch (reloadError) {
                        console.error('Failed to reload tab after environment switch:', reloadError);
                        window.showToast('Failed to refresh tab after switching configuration', 'error');
                    }
                }
            }
            // For environments tab, no refresh needed as it already shows the active config
        },
        async fetchActiveEnvironmentConfig() {
            const active = sanitizeConfigName(this.activeEnvironment);
            if (active !== this.activeEnvironment) {
                this.activeEnvironment = active;
            }
            if (!active) {
                this.activeEnvironmentConfig = null;
                this.activeEnvironmentMetadata = null;
                await this.updateModelContextFromConfig(null, null);
                return null;
            }

            try {
                const response = await fetch(`/api/configs/${encodeURIComponent(active)}?t=${Date.now()}`);
                if (!response.ok) {
                    throw new Error(`Failed to load configuration for ${active}`);
                }

                const payload = await response.json();
                const rawConfig = payload?.config || {};
                const metadata = payload?.metadata || null;
                const canonicalConfig = this.normalizeConfigInput(rawConfig);
                this.activeEnvironmentConfig = canonicalConfig;
                this.activeEnvironmentMetadata = metadata;
                this.configValues = { ...canonicalConfig };
                this.seedFormValueStoreFromConfig(canonicalConfig);
                await this.updateModelContextFromConfig(canonicalConfig, metadata);
                return payload;
            } catch (error) {
                console.error('Error loading active environment config:', error);
                this.activeEnvironmentConfig = null;
                this.activeEnvironmentMetadata = null;
                this.seedFormValueStoreFromConfig(null);
                await this.updateModelContextFromConfig(null, null);
                this.configValues = {};
                return null;
            }
        },
        async openConfigJsonModal() {
            this.configJsonError = '';
            const activeName = sanitizeConfigName(this.activeEnvironment);
            if (!activeName) {
                window.showToast && window.showToast('Select a configuration first', 'warning');
                return;
            }

            // Always refresh configuration to ensure we have the latest values
            try {
                await this.fetchActiveEnvironmentConfig();
            } catch (error) {
                console.error('Failed to fetch active configuration before opening JSON modal:', error);
            }

            if (!this.activeEnvironmentConfig || typeof this.activeEnvironmentConfig !== 'object') {
                window.showToast && window.showToast('Unable to load configuration JSON', 'error');
                return;
            }

            if (!this.resetConfigJsonDraft()) {
                return;
            }

            this.showConfigJsonModal = true;
        },
        closeConfigJsonModal() {
            this.showConfigJsonModal = false;
            this.configJsonError = '';
        },
        toBareConfigKey(rawKey) {
            if (rawKey === null || rawKey === undefined) {
                return null;
            }
            const stringKey = typeof rawKey === 'string' ? rawKey : String(rawKey);
            const trimmed = stringKey.trim();
            if (!trimmed) {
                return null;
            }
            if (trimmed.startsWith('_')) {
                return trimmed;
            }
            const withoutPrefix = trimmed.replace(/^[-]+/, '').trim();
            return withoutPrefix || trimmed;
        },
        isCliConfigKey(rawKey) {
            return typeof rawKey === 'string' && rawKey.trim().startsWith('--');
        },
        normalizeConfigForJson(config) {
            if (!config || typeof config !== 'object' || Array.isArray(config)) {
                return {};
            }
            const map = new Map();
            Object.entries(config).forEach(([key, value]) => {
                const bare = this.toBareConfigKey(key);
                if (!bare) {
                    return;
                }
                const isCli = this.isCliConfigKey(key);
                // Prefer bare keys over CLI dashed keys for the JSON view
                if (!map.has(bare) || !isCli) {
                    map.set(bare, value);
                }
            });
            const sortedKeys = Array.from(map.keys()).sort((a, b) => a.localeCompare(b));
            const normalized = {};
            sortedKeys.forEach((key) => {
                normalized[key] = map.get(key);
            });
            return normalized;
        },
        normalizeConfigInput(config) {
            if (!config || typeof config !== 'object' || Array.isArray(config)) {
                return {};
            }
            const normalized = {};
            Object.entries(config).forEach(([key, value]) => {
                // Ensure keys are stored in the canonical internal format (dashed)
                // This matches how fields are stored in FormData and processed by the backend
                const canonical = this.canonicalizeKey(key);
                if (!canonical) {
                    return;
                }
                normalized[canonical] = value;
            });
            return normalized;
        },
        formatConfigJsonDraft() {
            try {
                const formatted = JSON.stringify(JSON.parse(this.configJsonDraft), null, 2);
                this.configJsonDraft = formatted;
                this.configJsonError = '';
            } catch (error) {
                this.configJsonError = `Invalid JSON: ${error.message}`;
            }
        },
        resetConfigJsonDraft() {
            try {
                const normalized = this.normalizeConfigForJson(this.activeEnvironmentConfig);
                this.configJsonDraft = JSON.stringify(normalized, null, 2);
                this.configJsonError = '';
                return true;
            } catch (error) {
                console.error('Failed to reset configuration JSON draft:', error);
                window.showToast && window.showToast('Unable to reset JSON draft', 'error');
                return false;
            }
        },
        async copyConfigJsonDraft() {
            const value = this.configJsonDraft || '';
            if (!value) {
                window.showToast && window.showToast('Nothing to copy', 'info');
                return;
            }

            try {
                if (window.navigator?.clipboard?.writeText) {
                    await window.navigator.clipboard.writeText(value);
                } else {
                    throw new Error('Clipboard API unavailable');
                }
                window.showToast && window.showToast('Configuration copied to clipboard', 'success');
            } catch (error) {
                console.warn('Clipboard API failed, attempting fallback copy.', error);
                try {
                    const textarea = document.createElement('textarea');
                    textarea.value = value;
                    textarea.setAttribute('readonly', '');
                    textarea.style.position = 'absolute';
                    textarea.style.left = '-9999px';
                    document.body.appendChild(textarea);
                    textarea.select();
                    const successful = document.execCommand('copy');
                    document.body.removeChild(textarea);
                    if (successful) {
                        window.showToast && window.showToast('Configuration copied to clipboard', 'success');
                        return;
                    }
                    throw new Error('execCommand copy returned false');
                } catch (fallbackError) {
                    console.error('Fallback clipboard copy failed:', fallbackError);
                    window.showToast && window.showToast('Copy failed. Please use Ctrl/Cmd+C.', 'warning');
                }
            }
        },
        async applyConfigJsonDraft() {
            this.configJsonError = '';
            let parsed;
            try {
                parsed = JSON.parse(this.configJsonDraft);
            } catch (error) {
                this.configJsonError = `Invalid JSON: ${error.message}`;
                return;
            }

            if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
                this.configJsonError = 'Configuration must be a JSON object.';
                return;
            }

            const normalized = this.normalizeConfigInput(parsed);
            this.activeEnvironmentConfig = normalized;
            this.formValueStore = {};
            this.configValues = { ...normalized };
            this.seedFormValueStoreFromConfig(normalized);
            await this.$nextTick();
            this.applyStoredValues();
            this.captureFormValues();
            this.markFormDirty();

            try {
                await this.updateModelContextFromConfig(normalized, this.activeEnvironmentMetadata);
            } catch (error) {
                console.error('Failed to update model context after JSON apply:', error);
            }

            try {
                await this.loadDatasetsAfterEnvironmentChange();
            } catch (error) {
                console.error('Failed to refresh datasets after JSON apply:', error);
            }

            this.showConfigJsonModal = false;
            window.dispatchEvent(new CustomEvent('environment-config-updated', {
                detail: {
                    environment: this.activeEnvironment,
                    source: 'json-edit',
                    config: normalized
                }
            }));
            window.showToast && window.showToast('Configuration updated from JSON', 'success');
        },
        async updateModelContextFromConfig(config, metadata) {
            const info = this.extractModelInfo(config, metadata);
            this.activeModelFamily = info.modelFamily;
            this.activeModelType = info.modelType;
            this.activeModelFlavour = info.modelFlavour;
            this.activeLoraType = info.loraType;

            if (!info.modelFamily) {
                this.modelDetails = null;
                this.modelContext = this.defaultModelContext();
                return;
            }

            if (!this._modelDetailsCache[info.modelFamily]) {
                try {
                    const response = await fetch(`/api/models/${info.modelFamily}`);
                    if (!response.ok) {
                        throw new Error(`Failed to fetch model metadata for ${info.modelFamily}`);
                    }
                    this._modelDetailsCache[info.modelFamily] = await response.json();
                } catch (error) {
                    console.error('Error loading model metadata:', error);
                    this._modelDetailsCache[info.modelFamily] = null;
                }
            }

            this.modelDetails = this._modelDetailsCache[info.modelFamily];
            const context = this.computeModelContext(info, config, this.modelDetails);
            const videoCapabilities = this.detectVideoCapabilities(info, config, metadata, this.modelDetails);
            context.isVideoModel = videoCapabilities.isVideoModel;
            context.supportsVideo = videoCapabilities.supportsVideo;
            console.debug(
                '[ModelContext] video capability evaluation',
                {
                    family: info.modelFamily,
                    flavour: info.modelFlavour,
                    rawCapabilities: this.modelDetails?.capabilities,
                    resolvedVideo: videoCapabilities,
                },
            );
            const strictI2VInfo = this.evaluateStrictI2VModel(
                this.modelDetails?.capabilities,
                info.modelFlavour,
                info.modelFamily,
                videoCapabilities,
            );
            console.debug(
                '[ModelContext] strict I2V evaluation',
                {
                    family: info.modelFamily,
                    flavour: info.modelFlavour,
                    capabilities: this.modelDetails?.capabilities,
                    videoCapabilities,
                    strictInfo: strictI2VInfo,
                },
            );
            context.requiresStrictI2VDatasets = strictI2VInfo.requiresStrictI2V;
            context.strictI2VFlavours = strictI2VInfo.strictFlavours;
            context.strictI2VActive = strictI2VInfo.active;

            const requirementPayload = this.buildModelRequirementPayload(info, config, metadata);
            if (requirementPayload) {
                const requirementOverrides = await this.evaluateModelRequirements(requirementPayload);
                if (requirementOverrides) {
                    if (Object.prototype.hasOwnProperty.call(requirementOverrides, 'requires_conditioning_dataset')) {
                        context.requiresConditioningDataset = Boolean(requirementOverrides.requires_conditioning_dataset);
                    }
                    if (Object.prototype.hasOwnProperty.call(requirementOverrides, 'requires_conditioning_latents')) {
                        context.requiresConditioningLatents = Boolean(requirementOverrides.requires_conditioning_latents);
                    }
                    if (Object.prototype.hasOwnProperty.call(requirementOverrides, 'requires_conditioning_image_embeds')) {
                        context.requiresConditioningImageEmbeds = Boolean(
                            requirementOverrides.requires_conditioning_image_embeds,
                        );
                    }
                    if (Object.prototype.hasOwnProperty.call(requirementOverrides, 'requires_conditioning_validation_inputs')) {
                        context.requiresConditioningValidationInputs = Boolean(
                            requirementOverrides.requires_conditioning_validation_inputs,
                        );
                    }
                    if (Object.prototype.hasOwnProperty.call(requirementOverrides, 'requires_validation_edit_captions')) {
                        context.requiresValidationEditCaptions = Boolean(
                            requirementOverrides.requires_validation_edit_captions,
                        );
                    }
                    if (Object.prototype.hasOwnProperty.call(requirementOverrides, 'requires_validation_i2v_samples')) {
                        context.requiresValidationI2VSamples = Boolean(
                            requirementOverrides.requires_validation_i2v_samples,
                        );
                    }
                    if (requirementOverrides.conditioning_dataset_type) {
                        context.conditioningDatasetType = requirementOverrides.conditioning_dataset_type;
                    }
                }
            }
            context.supportsConditioningGenerators = Boolean(
                context.supportsConditioningGenerators
                || context.controlnetEnabled
                || context.controlEnabled
                || context.requiresConditioningDataset,
            );

            this.modelContext = { ...this.defaultModelContext(), ...context };
            try {
                window.dispatchEvent(new CustomEvent('trainer-model-context-updated', {
                    detail: this.modelContext,
                }));
            } catch (error) {
                console.debug('Failed to dispatch model context event', error);
            }
        },
        defaultModelContext() {
            return {
                requiresConditioningDataset: false,
                requiresConditioningLatents: false,
                requiresConditioningImageEmbeds: false,
                requiresConditioningValidationInputs: false,
                requiresValidationEditCaptions: false,
                requiresValidationI2VSamples: false,
                requiresStrictI2VDatasets: false,
                strictI2VActive: false,
                strictI2VFlavours: [],
                conditioningDatasetType: 'conditioning',
                supportsConditioningGenerators: false,
                hasControlnetPipeline: false,
                modelFlavour: null,
                controlnetEnabled: false,
                controlEnabled: false,
                isVideoModel: false,
                supportsVideo: false,
            };
        },
        extractModelInfo(config, metadata) {
            const normalized = this.buildNormalizedConfig(config);
            const modelFamily = this.firstNonEmpty(
                normalized.model_family,
                normalized.modelfamily,
                normalized.model,
                metadata?.model_family,
                metadata?.modelfamily,
            );
            const modelType = this.firstNonEmpty(
                normalized.model_type,
                metadata?.model_type,
            );
            const modelFlavour = this.firstNonEmpty(
                normalized.model_flavour,
                normalized.modelflavour,
                metadata?.model_flavour,
            );
            const loraType = this.firstNonEmpty(
                normalized.lora_type,
                metadata?.lora_type,
            );
            const controlnet = this.normalizeBoolean(normalized.controlnet);
            const control = this.normalizeBoolean(normalized.control);

            return {
                modelFamily: modelFamily || null,
                modelType: modelType || null,
                modelFlavour: modelFlavour || null,
                loraType: loraType || null,
                controlnet,
                control,
            };
        },
        buildNormalizedConfig(rawConfig) {
            const normalized = {};
            if (!rawConfig || typeof rawConfig !== 'object') {
                return normalized;
            }

            const addKeyVariants = (key, value) => {
                if (!key) {
                    return;
                }
                const variants = new Set();
                variants.add(key);
                const trimmed = key.startsWith('--') ? key.slice(2) : key;
                variants.add(trimmed);
                variants.add(trimmed.toLowerCase());
                const snake = trimmed
                    .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
                    .replace(/[-\s]+/g, '_')
                    .toLowerCase();
                variants.add(snake);
                variants.add(snake.replace(/__/g, '_'));
                variants.add(snake.replace(/_/g, ''));

                variants.forEach((variant) => {
                    if (variant) {
                        normalized[variant] = value;
                    }
                });
            };

            Object.entries(rawConfig).forEach(([key, value]) => {
                addKeyVariants(key, value);
            });

            return normalized;
        },
        computeModelContext(info, config, details) {
            const normalized = this.buildNormalizedConfig(config);
            const controlnetEnabled = this.normalizeBoolean(info.controlnet || normalized.controlnet);
            const controlEnabled = this.normalizeBoolean(info.control || normalized.control);
            const modelFamily = info.modelFamily || '';
            const modelFlavour = info.modelFlavour || '';
            const fluxKontext = modelFamily === 'flux' && modelFlavour === 'kontext';

            const requiresDataset = Boolean(controlnetEnabled || controlEnabled || fluxKontext);
            const requiresLatents = Boolean(controlnetEnabled || controlEnabled || fluxKontext);
            const requiresValidationInputs = Boolean(controlnetEnabled || fluxKontext);
            const requiresValidationEditCaptions = Boolean(controlnetEnabled || fluxKontext);
            const conditioningDatasetType = fluxKontext ? 'image' : 'conditioning';
            const hasControlnetPipeline = Boolean(details?.capabilities?.has_controlnet_pipeline);
            const supportsConditioningGenerators = Boolean(
                hasControlnetPipeline || controlnetEnabled || controlEnabled || fluxKontext,
            );

            return {
                requiresConditioningDataset: requiresDataset,
                requiresConditioningLatents: requiresLatents,
                requiresConditioningImageEmbeds: false,
                requiresConditioningValidationInputs: requiresValidationInputs,
                requiresValidationEditCaptions,
                conditioningDatasetType,
                supportsConditioningGenerators,
                hasControlnetPipeline,
                modelFlavour: modelFlavour || null,
                controlnetEnabled,
                controlEnabled,
            };
        },
        detectVideoCapabilities(info, config, metadata, details) {
            const capabilities = details?.capabilities || {};
            const attributes = details?.attributes || {};
            const normalizedConfig = this.buildNormalizedConfig(config);
            const normalizedMetadata = this.buildNormalizedConfig(metadata || {});
            const flags = [
                capabilities.is_video_model,
                capabilities.supports_video,
                attributes.is_video_model,
                attributes.supports_video,
                normalizedConfig.is_video_model,
                normalizedMetadata.is_video_model,
            ];
            const hasVideoFlag = flags.some((value) => this.normalizeBoolean(value));
            const typeHints = [
                info?.modelType,
                normalizedConfig.model_type,
                normalizedMetadata.model_type,
                metadata?.model_type,
            ].map((value) => (typeof value === 'string' ? value.toLowerCase() : ''));
            const isVideoType = typeHints.some((value) => ['video', 'video_model', 'video-diffusion', 'video_diffusion'].includes(value));
            const family = typeof info?.modelFamily === 'string' ? info.modelFamily.toLowerCase() : '';
            const knownVideoFamilies = new Set(['wan', 'ltxvideo', 'cosmos2image']);
            const isVideoFamily = knownVideoFamilies.has(family);
            const isVideoModel = Boolean((hasVideoFlag || isVideoType || isVideoFamily) && isVideoFamily);
            const supportsVideo = Boolean(this.normalizeBoolean(capabilities.supports_video) && isVideoFamily);
            return { isVideoModel, supportsVideo };
        },
        evaluateStrictI2VModel(capabilities, modelFlavour, modelFamily = null, videoCapabilities = null) {
            const result = {
                requiresStrictI2V: false,
                strictFlavours: [],
                active: false,
            };
            if (!capabilities || typeof capabilities !== 'object') {
                return result;
            }
            const familyKey = typeof modelFamily === 'string' ? modelFamily.trim().toLowerCase() : '';
            const knownVideoFamilies = new Set(['wan', 'ltxvideo', 'cosmos2image']);
            const isKnownVideoFamily = knownVideoFamilies.has(familyKey);
            const strictFlavoursRaw = Array.isArray(capabilities.strict_i2v_flavours)
                ? capabilities.strict_i2v_flavours
                : [];
            const strictFlavours = strictFlavoursRaw
                .map((value) => (typeof value === 'string' ? value.trim() : ''))
                .filter((value, index, arr) => value && arr.indexOf(value) === index);
            const strictForAll = this.normalizeBoolean(capabilities.strict_i2v_all_flavours);
            const flavourKey = typeof modelFlavour === 'string' ? modelFlavour.trim().toLowerCase() : '';
            const matchesFlavour = flavourKey
                ? strictFlavours.some((entry) => entry.toLowerCase() === flavourKey)
                : false;
            result.strictFlavours = strictFlavours;
            const requires = Boolean(strictForAll || matchesFlavour);
            result.requiresStrictI2V = requires && isKnownVideoFamily;
            result.active = result.requiresStrictI2V && isKnownVideoFamily;
            return result;
        },
        cloneConfigForPayload(source) {
            if (!source || typeof source !== 'object') {
                return {};
            }
            try {
                return JSON.parse(JSON.stringify(source));
            } catch (error) {
                console.warn('Falling back to shallow clone for model requirement payload', error);
                return { ...source };
            }
        },
        buildModelRequirementPayload(info, config, metadata) {
            if (!info || !info.modelFamily) {
                return null;
            }

            const payloadConfig = this.cloneConfigForPayload(config);
            if (info.modelFamily && payloadConfig.model_family === undefined) {
                payloadConfig.model_family = info.modelFamily;
            }
            if (info.modelFlavour && payloadConfig.model_flavour === undefined) {
                payloadConfig.model_flavour = info.modelFlavour;
            }
            if (info.modelType && payloadConfig.model_type === undefined) {
                payloadConfig.model_type = info.modelType;
            }
            if (info.controlnet !== undefined && payloadConfig.controlnet === undefined) {
                payloadConfig.controlnet = info.controlnet;
            }
            if (info.control !== undefined && payloadConfig.control === undefined) {
                payloadConfig.control = info.control;
            }

            const payload = {
                model_family: info.modelFamily,
                config: payloadConfig,
            };

            if (metadata && typeof metadata === 'object' && Object.keys(metadata).length > 0) {
                payload.metadata = metadata;
            }

            return payload;
        },
        async evaluateModelRequirements(payload) {
            if (!payload || !payload.model_family) {
                return null;
            }

            try {
                const response = await fetch('/api/models/requirements', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify(payload),
                });

                if (!response.ok) {
                    throw new Error(`Failed with status ${response.status}`);
                }

                return await response.json();
            } catch (error) {
                console.error('Failed to evaluate model requirements:', error);
                return null;
            }
        },
        normalizeBoolean(value) {
            if (value === null || value === undefined) {
                return false;
            }
            if (typeof value === 'boolean') {
                return value;
            }
            if (typeof value === 'number') {
                return value !== 0;
            }
            if (typeof value === 'string') {
                const trimmed = value.trim().toLowerCase();
                if (!trimmed) {
                    return false;
                }
                if (['true', '1', 'yes', 'on'].includes(trimmed)) {
                    return true;
                }
                if (['false', '0', 'no', 'off'].includes(trimmed)) {
                    return false;
                }
            }
            return Boolean(value);
        },
        firstNonEmpty(...values) {
            for (const value of values) {
                if (value === null || value === undefined) {
                    continue;
                }
                if (typeof value === 'string') {
                    const trimmed = value.trim();
                    if (trimmed) {
                        return trimmed;
                    }
                } else if (value) {
                    return value;
                }
            }
            return null;
        },
        normalizeDatasets(datasets) {
            if (!Array.isArray(datasets)) {
                return [];
            }
            return datasets.map((dataset, index) => this.normalizeDataset(dataset, index));
        },
        normalizeDataset(dataset, index = 0) {
            if (!dataset || typeof dataset !== 'object') {
                return dataset;
            }

            if (!dataset.id || typeof dataset.id !== 'string') {
                dataset.id = dataset.id ? String(dataset.id) : `dataset-${Date.now()}-${index}`;
            }

            dataset.type = dataset.type || 'local';
            if (!dataset.dataset_type || typeof dataset.dataset_type !== 'string' || !dataset.dataset_type.trim()) {
                dataset.dataset_type = 'image';
            }

            dataset.conditioning_data = this.normalizeConditioningData(dataset.conditioning_data);
            dataset.conditioning = this.normalizeConditioningGenerators(dataset.conditioning, dataset.id);

            if (dataset.dataset_type === 'video') {
                dataset.video = this.normalizeVideoOptions(dataset.video);
            } else if (dataset.video && typeof dataset.video === 'object') {
                dataset.video = this.normalizeVideoOptions(dataset.video);
            }

            if (dataset.dataset_type === 'conditioning') {
                dataset.conditioning_type = dataset.conditioning_type || 'controlnet';
                dataset.default = this.normalizeBoolean(dataset.default);
            }

            if (dataset.dataset_type === 'text_embeds' || dataset.dataset_type === 'image_embeds') {
                dataset.cache_dir = dataset.cache_dir || dataset.instance_data_dir || '';
            } else {
                dataset.instance_data_dir = dataset.instance_data_dir || '';
                if (dataset.resolution === undefined || dataset.resolution === null) {
                    dataset.resolution = 1024;
                }
            }

            if (dataset.crop_style === 'balanced') {
                dataset.crop_style = 'random';
            }

            if (dataset.crop_style === 'centre') {
                dataset.crop_style = 'center';
            }

            if (dataset.crop_aspect === 'original') {
                dataset.crop_aspect = 'preserve';
            }

            const allowedCropStyles = ['center', 'corner', 'face', 'random'];
            if (!allowedCropStyles.includes(dataset.crop_style)) {
                dataset.crop_style = 'random';
            }

            const allowedCropAspects = ['square', 'preserve', 'closest', 'random'];
            if (!allowedCropAspects.includes(dataset.crop_aspect)) {
                dataset.crop_aspect = 'square';
            }

            const allowedResolutionTypes = ['pixel', 'pixel_area'];
            if (!allowedResolutionTypes.includes(dataset.resolution_type)) {
                dataset.resolution_type = 'pixel';
            }

            if (!dataset.parquet || typeof dataset.parquet !== 'object' || Array.isArray(dataset.parquet)) {
                dataset.parquet = {};
            }

            if (dataset.type === 'csv' && (!dataset.caption_strategy || dataset.caption_strategy === 'textfile')) {
                dataset.caption_strategy = 'csv';
            }
            if (dataset.type === 'huggingface') {
                if (!dataset.caption_strategy || dataset.caption_strategy === 'textfile') {
                    dataset.caption_strategy = 'huggingface';
                }
                if (!dataset.metadata_backend || dataset.metadata_backend === '' || dataset.metadata_backend === 'discovery') {
                    dataset.metadata_backend = 'huggingface';
                }
            }

            const repeats = this._coerceInt(dataset.repeats);
            dataset.repeats = Number.isFinite(repeats) && repeats >= 0 ? repeats : 0;

            const probability = this._coerceFloat(dataset.probability);
            dataset.probability = Number.isFinite(probability)
                ? Math.max(0, Math.min(1, probability))
                : 1;

            const minAspect = this._coerceFloat(dataset.minimum_aspect_ratio);
            dataset.minimum_aspect_ratio = Number.isFinite(minAspect) && minAspect > 0 ? minAspect : null;

            const maxAspect = this._coerceFloat(dataset.maximum_aspect_ratio);
            dataset.maximum_aspect_ratio = Number.isFinite(maxAspect) && maxAspect > 0 ? maxAspect : null;

            const buckets = this.parseAspectBucketText(
                typeof dataset._cropAspectBucketsText === 'string' && dataset._cropAspectBucketsText.trim() !== ''
                    ? dataset._cropAspectBucketsText
                    : dataset.crop_aspect_buckets,
            );
            dataset.crop_aspect_buckets = buckets;
            dataset._cropAspectBucketsText = buckets.length ? buckets.join(', ') : '';

            if (Array.isArray(dataset.skip_file_discovery)) {
                dataset.skip_file_discovery = dataset.skip_file_discovery.join(' ');
            } else if (typeof dataset.skip_file_discovery !== 'string') {
                dataset.skip_file_discovery = '';
            }
            dataset.skip_file_discovery = this.normalizeSkipList(dataset.skip_file_discovery);

            if (dataset.is_regularisation_data === undefined && dataset.is_regularization_data !== undefined) {
                dataset.is_regularisation_data = dataset.is_regularization_data;
            }

            dataset.hash_filenames = this.normalizeBoolean(dataset.hash_filenames);
            dataset.preserve_data_backend_cache = this.normalizeBoolean(dataset.preserve_data_backend_cache);
            dataset.is_regularisation_data = this.normalizeBoolean(dataset.is_regularisation_data);
            dataset.is_regularization_data = dataset.is_regularisation_data;
            dataset.prepend_instance_prompt = this.normalizeBoolean(dataset.prepend_instance_prompt);
            dataset.only_instance_prompt = this.normalizeBoolean(dataset.only_instance_prompt);
            dataset.disabled = this.normalizeBoolean(dataset.disabled);

            dataset.instance_prompt = this._coerceString(dataset.instance_prompt);
            dataset.cache_dir_vae = this._coerceString(dataset.cache_dir_vae);
            dataset.caption_filter_list = this._coerceString(dataset.caption_filter_list);
            if (dataset.dataset_type === 'text_embeds') {
                dataset._selectedCaptionFilter = dataset.caption_filter_list ? '__custom__' : '';
            } else {
                dataset.caption_filter_list = '';
                dataset._selectedCaptionFilter = '';
            }

            dataset.text_embeds = this._coerceString(dataset.text_embeds);
            dataset.image_embeds = this._coerceString(dataset.image_embeds);
            if (!['image', 'video'].includes(dataset.dataset_type)) {
                dataset.text_embeds = '';
                dataset.image_embeds = '';
            }

            dataset.aws_bucket_name = this._coerceString(dataset.aws_bucket_name);
            dataset.aws_data_prefix = this._coerceString(dataset.aws_data_prefix);
            dataset.aws_region_name = this._coerceString(dataset.aws_region_name);
            dataset.aws_endpoint_url = this._coerceString(dataset.aws_endpoint_url);
            dataset.aws_access_key_id = this._coerceString(dataset.aws_access_key_id);
            dataset.aws_secret_access_key = this._coerceString(dataset.aws_secret_access_key);
            dataset._awsAccessVisible = false;
            dataset._awsSecretVisible = false;
            if (dataset.type !== 'aws') {
                dataset.aws_access_key_id = '';
                dataset.aws_secret_access_key = '';
                dataset.aws_endpoint_url = '';
                dataset.aws_region_name = '';
                dataset.aws_data_prefix = '';
            }

            dataset.csv_file = this._coerceString(dataset.csv_file);
            dataset.csv_caption_column = this._coerceString(dataset.csv_caption_column);
            dataset.csv_cache_dir = this._coerceString(dataset.csv_cache_dir);

            dataset.dataset_name = this._coerceString(dataset.dataset_name);
            dataset.dataset_config = this._coerceString(dataset.dataset_config);
            dataset.split = this._coerceString(dataset.split);
            dataset.revision = this._coerceString(dataset.revision);
            dataset.image_column = this._coerceString(dataset.image_column);
            dataset.caption_column = this._coerceString(dataset.caption_column);
            dataset.streaming = false;
            dataset.auth_token = this._coerceString(dataset.auth_token);

            if (dataset.dataset_type === 'text_embeds' || dataset.dataset_type === 'image_embeds') {
                const writeBatch = this._coerceInt(dataset.write_batch_size);
                dataset.write_batch_size = Number.isFinite(writeBatch) && writeBatch > 0 ? writeBatch : 128;
            } else {
                delete dataset.write_batch_size;
            }

            dataset._showAdvanced = Boolean(dataset._showAdvanced);

            if (dataset._connectionStatus === undefined) {
                dataset._connectionStatus = null;
            }
            if (dataset._connectionMessage === undefined) {
                dataset._connectionMessage = '';
            }
            if (dataset._connectionDetails === undefined) {
                dataset._connectionDetails = null;
            }
            if (dataset._connectionTesting === undefined) {
                dataset._connectionTesting = false;
            }

            return dataset;
        },
        normalizeVideoOptions(value) {
            const result = {};
            if (value && typeof value === 'object') {
                const numFrames = this._coerceInt(value.num_frames);
                const minFrames = this._coerceInt(value.min_frames);
                const maxFrames = this._coerceInt(value.max_frames);
                if (Number.isFinite(numFrames) && numFrames > 0) {
                    result.num_frames = numFrames;
                }
                if (Number.isFinite(minFrames) && minFrames > 0) {
                    result.min_frames = minFrames;
                }
                if (Number.isFinite(maxFrames) && maxFrames > 0) {
                    result.max_frames = maxFrames;
                }
                if (typeof value.is_i2v === 'boolean') {
                    result.is_i2v = value.is_i2v;
                } else if (typeof value.is_i2v === 'string' && value.is_i2v.trim() !== '') {
                    result.is_i2v = value.is_i2v.trim().toLowerCase() === 'true';
                }
            }
            return result;
        },
        _coerceInt(value) {
            if (value === null || value === undefined || value === '') {
                return NaN;
            }
            if (typeof value === 'number') {
                return Number.isFinite(value) ? value : NaN;
            }
            const next = Number.parseInt(String(value), 10);
            return Number.isFinite(next) ? next : NaN;
        },
        _coerceFloat(value) {
            if (value === null || value === undefined || value === '') {
                return NaN;
            }
            if (typeof value === 'number') {
                return Number.isFinite(value) ? value : NaN;
            }
            const next = Number.parseFloat(String(value));
            return Number.isFinite(next) ? next : NaN;
        },
        _coerceString(value) {
            if (value === null || value === undefined) {
                return '';
            }
            return String(value).trim();
        },
        isKnownConditioningGenerator(type) {
            if (!type) {
                return false;
            }
            return this.knownConditioningGenerators.includes(type);
        },
        parseAspectBucketText(text) {
            if (Array.isArray(text)) {
                const asNumbers = text
                    .map((value) => this._coerceFloat(value))
                    .filter((value) => Number.isFinite(value) && value > 0);
                const deduped = [];
                const seen = new Set();
                asNumbers.forEach((value) => {
                    const key = value.toFixed(4);
                    if (!seen.has(key)) {
                        seen.add(key);
                        deduped.push(value);
                    }
                });
                return deduped;
            }
            if (typeof text !== 'string') {
                return [];
            }
            const seen = new Set();
            const buckets = [];
            text
                .split(/[,\s]+/)
                .map((entry) => this._coerceFloat(entry))
                .forEach((value) => {
                    if (!Number.isFinite(value) || value <= 0) {
                        return;
                    }
                    const key = value.toFixed(4);
                    if (!seen.has(key)) {
                        seen.add(key);
                        buckets.push(value);
                    }
                });
            return buckets;
        },
        normalizeSkipList(value) {
            if (Array.isArray(value)) {
                return value
                    .map((entry) => this._coerceString(entry))
                    .filter((entry) => entry !== '')
                    .join(' ');
            }
            if (typeof value !== 'string') {
                return '';
            }
            const parts = value
                .split(/[,\s]+/)
                .map((entry) => entry.trim())
                .filter((entry) => entry.length > 0);
            return parts.join(' ');
        },
        normalizeConditioningData(value) {
            if (Array.isArray(value)) {
                return value
                    .map((item) => (typeof item === 'string' ? item.trim() : String(item)))
                    .filter((item) => item !== '')
                    .filter((item, idx, arr) => arr.indexOf(item) === idx);
            }

            if (typeof value === 'string') {
                const trimmed = value.trim();
                return trimmed ? [trimmed] : [];
            }

            if (value && typeof value === 'object') {
                try {
                    return Object.values(value)
                        .map((item) => (typeof item === 'string' ? item.trim() : String(item)))
                        .filter((item) => item !== '');
                } catch (error) {
                    console.warn('Unable to normalize conditioning_data value', error);
                }
            }

            return [];
        },
        normalizeConditioningGenerators(value, datasetId) {
            let generators = [];
            if (Array.isArray(value)) {
                generators = value;
            } else if (value && typeof value === 'object') {
                generators = [value];
            } else if (typeof value === 'string') {
                try {
                    const parsed = JSON.parse(value);
                    if (Array.isArray(parsed)) {
                        generators = parsed;
                    }
                } catch (error) {
                    console.warn('Failed to parse conditioning JSON string:', error);
                }
            }

            return generators.map((entry, index) => this.normalizeConditioningEntry(entry, datasetId, index));
        },
        normalizeConditioningEntry(entry, datasetId, index) {
            const normalized = (entry && typeof entry === 'object') ? { ...entry } : {};
            const rawType = typeof normalized.type === 'string' ? normalized.type.trim() : '';
            normalized.type = rawType;
            if (this.isKnownConditioningGenerator(rawType)) {
                normalized._selectedType = rawType;
                normalized._customType = '';
            } else if (rawType) {
                normalized._selectedType = '__custom__';
                normalized._customType = rawType;
            } else {
                normalized._selectedType = '';
                normalized._customType = '';
            }
            normalized.id = typeof normalized.id === 'string' && normalized.id.trim()
                ? normalized.id.trim()
                : `${datasetId || 'dataset'}-conditioning-${index}`;

            // Normalize params
            if (normalized.params && typeof normalized.params === 'string') {
                try {
                    normalized.params = JSON.parse(normalized.params);
                    normalized._paramsParseError = null;
                } catch (error) {
                    console.warn('Failed to parse conditioning params:', error);
                    normalized._paramsParseError = 'Invalid JSON';
                    normalized.params = {};
                }
            }

            if (!normalized.params || typeof normalized.params !== 'object' || Array.isArray(normalized.params)) {
                normalized.params = {};
            }

            normalized._paramsText = JSON.stringify(normalized.params, null, 2);
            normalized._paramsParseError = null;

            // Normalize captions
            const captions = normalized.captions;
            if (captions === false) {
                normalized._captionsMode = 'none';
                normalized._captionsValue = '';
            } else if (Array.isArray(captions)) {
                normalized._captionsMode = 'list';
                normalized._captionsValue = captions.map((value) => (value || '').toString()).join('\n');
            } else if (typeof captions === 'string') {
                const trimmed = captions.trim();
                if (trimmed) {
                    normalized._captionsMode = 'single';
                    normalized._captionsValue = trimmed;
                } else {
                    normalized._captionsMode = 'inherit';
                    normalized._captionsValue = '';
                    delete normalized.captions;
                }
            } else {
                normalized._captionsMode = 'inherit';
                normalized._captionsValue = '';
                if (captions !== undefined) {
                    delete normalized.captions;
                }
            }

            if (!normalized._uiKey) {
                normalized._uiKey = `${datasetId || 'dataset'}-${index}-${Math.random().toString(36).slice(2, 8)}`;
            }

            return normalized;
        },
        safeParseJSON(text) {
            if (typeof text !== 'string') {
                return null;
            }
            try {
                return JSON.parse(text);
            } catch (error) {
                return null;
            }
        },
        prepareDatasetsForSave(datasets = null) {
            const source = Array.isArray(datasets) ? datasets : this.datasets;
            if (!Array.isArray(source)) {
                return [];
            }

            const sanitizeCaptions = (entry) => {
                const mode = entry._captionsMode || 'inherit';
                const value = entry._captionsValue || '';
                if (mode === 'none') {
                    return false;
                }
                if (mode === 'single') {
                    const trimmed = value.trim();
                    return trimmed ? trimmed : undefined;
                }
                if (mode === 'list') {
                    const items = value
                        .split(/\r?\n/)
                        .map((line) => line.trim())
                        .filter((line) => line !== '');
                    return items.length ? items : undefined;
                }
                if (entry.captions === false) {
                    return false;
                }
                if (Array.isArray(entry.captions)) {
                    return entry.captions;
                }
                if (typeof entry.captions === 'string' && entry.captions.trim()) {
                    return entry.captions.trim();
                }
                return undefined;
            };

            const sanitizeConditioningEntry = (entry) => {
                if (!entry || typeof entry !== 'object') {
                    return {};
                }
                const cleaned = {};
                Object.entries(entry).forEach(([key, value]) => {
                    if (key.startsWith('_')) {
                        return;
                    }
                    cleaned[key] = this.sanitizeValueForSave(value);
                });

                const parsedParams = this.safeParseJSON(entry._paramsText);
                if (parsedParams !== null) {
                    cleaned.params = parsedParams;
                } else if (entry.params && typeof entry.params === 'object' && !Array.isArray(entry.params)) {
                    cleaned.params = entry.params;
                } else {
                    cleaned.params = {};
                }

                const captions = sanitizeCaptions(entry);
                if (captions === undefined) {
                    delete cleaned.captions;
                } else {
                    cleaned.captions = captions;
                }

                return cleaned;
            };

            const conditioningIds = new Set(
                source
                    .filter((dataset) => dataset && dataset.dataset_type === 'conditioning')
                    .map((dataset) => (dataset.id ? String(dataset.id).trim() : ''))
                    .filter((id) => id !== '')
            );

            const textEmbedIds = new Set(
                source
                    .filter((dataset) => dataset && dataset.dataset_type === 'text_embeds')
                    .map((dataset) => (dataset.id ? String(dataset.id).trim() : ''))
                    .filter((id) => id !== '')
            );

            const imageEmbedIds = new Set(
                source
                    .filter((dataset) => dataset && dataset.dataset_type === 'image_embeds')
                    .map((dataset) => (dataset.id ? String(dataset.id).trim() : ''))
                    .filter((id) => id !== '')
            );

            const validationErrors = {};

            const sanitizeVideoOptions = (options) => {
                if (!options || typeof options !== 'object') {
                    return undefined;
                }
                const cleaned = {};
                const numFrames = this._coerceInt(options.num_frames);
                const minFrames = this._coerceInt(options.min_frames);
                const maxFrames = this._coerceInt(options.max_frames);
                if (Number.isFinite(numFrames) && numFrames > 0) {
                    cleaned.num_frames = numFrames;
                }
                if (Number.isFinite(minFrames) && minFrames > 0) {
                    cleaned.min_frames = minFrames;
                }
                if (Number.isFinite(maxFrames) && maxFrames > 0) {
                    cleaned.max_frames = maxFrames;
                }
                if (typeof options.is_i2v === 'boolean') {
                    cleaned.is_i2v = options.is_i2v;
                } else if (typeof options.is_i2v === 'string' && options.is_i2v.trim() !== '') {
                    cleaned.is_i2v = options.is_i2v.trim().toLowerCase() === 'true';
                }
                return Object.keys(cleaned).length ? cleaned : undefined;
            };

            const sanitizeDataset = (dataset) => {
                const datasetId = dataset?.id ? String(dataset.id) : 'dataset';
                const cleaned = {};
                Object.entries(dataset).forEach(([key, value]) => {
                    if (key.startsWith('_')) {
                        return;
                    }
                    if (key === 'conditioning') {
                        cleaned.conditioning = Array.isArray(value)
                            ? value.map((entry) => sanitizeConditioningEntry(entry))
                            : [];
                        return;
                    }
                    if (key === 'conditioning_data') {
                        const normalized = this.normalizeConditioningData(value);
                        cleaned.conditioning_data = normalized;
                        return;
                    }
                    cleaned[key] = this.sanitizeValueForSave(value);
                });

                if (!cleaned.type) {
                    cleaned.type = 'local';
                }

                if (!cleaned.dataset_type || typeof cleaned.dataset_type !== 'string' || !cleaned.dataset_type.trim()) {
                    cleaned.dataset_type = 'image';
                }

                if (cleaned.dataset_type === 'conditioning') {
                    cleaned.default = this.normalizeBoolean(cleaned.default);
                    if (!Array.isArray(cleaned.conditioning_data)) {
                        cleaned.conditioning_data = [];
                    }
                } else {
                    const links = Array.isArray(cleaned.conditioning_data) ? cleaned.conditioning_data : [];
                    cleaned.conditioning_data = links;
                    const invalid = links.filter((id) => !conditioningIds.has(id));
                    if (invalid.length) {
                        validationErrors[`${datasetId}.conditioning_data`] = `Unknown conditioning dataset(s): ${invalid.join(', ')}`;
                    }
                }

                if (cleaned.dataset_type === 'video') {
                    const videoOptions = sanitizeVideoOptions(dataset.video);
                    if (videoOptions) {
                        cleaned.video = videoOptions;
                    }
                }

                if (cleaned.type !== 'local') {
                    delete cleaned.instance_data_dir;
                }

                if (cleaned.crop_style === 'balanced') {
                    cleaned.crop_style = 'random';
                }

                if (cleaned.crop_style === 'centre') {
                    cleaned.crop_style = 'center';
                }

                if (cleaned.crop_aspect === 'original') {
                    cleaned.crop_aspect = 'preserve';
                }

                const allowedSanitizedCropStyles = ['center', 'corner', 'face', 'random'];
                if (!allowedSanitizedCropStyles.includes(cleaned.crop_style)) {
                    cleaned.crop_style = 'random';
                }

                const allowedSanitizedAspects = ['square', 'preserve', 'closest', 'random'];
                if (!allowedSanitizedAspects.includes(cleaned.crop_aspect)) {
                    cleaned.crop_aspect = 'square';
                }

                const allowedResolutionTypes = ['pixel', 'pixel_area'];
                if (!allowedResolutionTypes.includes(cleaned.resolution_type)) {
                    cleaned.resolution_type = 'pixel';
                }

                const repeatsValue = this._coerceInt(dataset.repeats);
                if (Number.isFinite(repeatsValue) && repeatsValue >= 0) {
                    cleaned.repeats = repeatsValue;
                } else {
                    delete cleaned.repeats;
                }

                const probabilityValue = this._coerceFloat(dataset.probability);
                if (Number.isFinite(probabilityValue)) {
                    cleaned.probability = Math.max(0, Math.min(1, probabilityValue));
                } else {
                    delete cleaned.probability;
                }

                if (cleaned.dataset_type === 'text_embeds' || cleaned.dataset_type === 'image_embeds') {
                    delete cleaned.minimum_aspect_ratio;
                    delete cleaned.maximum_aspect_ratio;
                    delete cleaned.crop_aspect_buckets;
                } else {
                    const minAspectValue = this._coerceFloat(dataset.minimum_aspect_ratio);
                    if (Number.isFinite(minAspectValue) && minAspectValue > 0) {
                        cleaned.minimum_aspect_ratio = minAspectValue;
                    } else {
                        delete cleaned.minimum_aspect_ratio;
                    }

                    const maxAspectValue = this._coerceFloat(dataset.maximum_aspect_ratio);
                    if (Number.isFinite(maxAspectValue) && maxAspectValue > 0) {
                        cleaned.maximum_aspect_ratio = maxAspectValue;
                    } else {
                        delete cleaned.maximum_aspect_ratio;
                    }

                    const bucketSource = typeof dataset._cropAspectBucketsText === 'string' && dataset._cropAspectBucketsText.trim() !== ''
                        ? dataset._cropAspectBucketsText
                        : dataset.crop_aspect_buckets;
                    const bucketValues = this.parseAspectBucketText(bucketSource);
                    if (bucketValues.length) {
                        cleaned.crop_aspect_buckets = bucketValues;
                    } else {
                        delete cleaned.crop_aspect_buckets;
                    }
                }

                const skipList = this.normalizeSkipList(dataset.skip_file_discovery);
                if (skipList) {
                    cleaned.skip_file_discovery = skipList;
                } else {
                    delete cleaned.skip_file_discovery;
                }

                cleaned.hash_filenames = this.normalizeBoolean(dataset.hash_filenames);
                cleaned.preserve_data_backend_cache = this.normalizeBoolean(dataset.preserve_data_backend_cache);
                cleaned.disabled = this.normalizeBoolean(dataset.disabled);

                const regularisation = this.normalizeBoolean(dataset.is_regularisation_data);
                if (regularisation) {
                    cleaned.is_regularisation_data = true;
                    cleaned.is_regularization_data = true; // ASSUMPTION: retain US spelling alias for compatibility
                } else {
                    delete cleaned.is_regularisation_data;
                    delete cleaned.is_regularization_data;
                }

                if (this.normalizeBoolean(dataset.prepend_instance_prompt)) {
                    cleaned.prepend_instance_prompt = true;
                } else {
                    delete cleaned.prepend_instance_prompt;
                }

                if (this.normalizeBoolean(dataset.only_instance_prompt)) {
                    cleaned.only_instance_prompt = true;
                } else {
                    delete cleaned.only_instance_prompt;
                }

                const instancePrompt = this._coerceString(dataset.instance_prompt);
                if (instancePrompt) {
                    cleaned.instance_prompt = instancePrompt;
                } else {
                    delete cleaned.instance_prompt;
                }

                const cacheDirVae = this._coerceString(dataset.cache_dir_vae);
                if (cacheDirVae) {
                    cleaned.cache_dir_vae = cacheDirVae;
                } else {
                    delete cleaned.cache_dir_vae;
                }

                if (cleaned.dataset_type === 'text_embeds' || cleaned.dataset_type === 'image_embeds') {
                    const writeBatchSize = this._coerceInt(dataset.write_batch_size);
                    if (Number.isFinite(writeBatchSize) && writeBatchSize > 0) {
                        cleaned.write_batch_size = writeBatchSize;
                    } else {
                        delete cleaned.write_batch_size;
                    }
                    const filterList = this._coerceString(dataset.caption_filter_list);
                    if (filterList) {
                        const resolvedFilter = typeof this.resolveCaptionFilterValue === 'function'
                            ? this.resolveCaptionFilterValue(filterList)
                            : filterList;
                        cleaned.caption_filter_list = resolvedFilter;
                    } else {
                        delete cleaned.caption_filter_list;
                    }
                } else {
                    delete cleaned.write_batch_size;
                    delete cleaned.caption_filter_list;
                }

                if (['image', 'video'].includes(cleaned.dataset_type)) {
                    const textEmbedRef = this._coerceString(dataset.text_embeds);
                    if (textEmbedRef) {
                        if (textEmbedIds.has(textEmbedRef)) {
                            cleaned.text_embeds = textEmbedRef;
                        } else {
                            delete cleaned.text_embeds;
                            validationErrors[`${datasetId}.text_embeds`] = `Unknown text embed dataset: ${textEmbedRef}`;
                        }
                    } else {
                        delete cleaned.text_embeds;
                    }
                    const imageEmbedRef = this._coerceString(dataset.image_embeds);
                    if (imageEmbedRef) {
                        if (imageEmbedIds.has(imageEmbedRef)) {
                            cleaned.image_embeds = imageEmbedRef;
                        } else {
                            delete cleaned.image_embeds;
                            validationErrors[`${datasetId}.image_embeds`] = `Unknown image embed dataset: ${imageEmbedRef}`;
                        }
                    } else {
                        delete cleaned.image_embeds;
                    }
                } else {
                    delete cleaned.text_embeds;
                    delete cleaned.image_embeds;
                }

                if (cleaned.type === 'aws') {
                    const bucket = this._coerceString(dataset.aws_bucket_name);
                    if (bucket) {
                        cleaned.aws_bucket_name = bucket;
                    } else {
                        delete cleaned.aws_bucket_name;
                    }
                    const dataPrefix = this._coerceString(dataset.aws_data_prefix);
                    if (dataPrefix) {
                        cleaned.aws_data_prefix = dataPrefix;
                    } else {
                        delete cleaned.aws_data_prefix;
                    }
                    const region = this._coerceString(dataset.aws_region_name);
                    if (region) {
                        cleaned.aws_region_name = region;
                    } else {
                        delete cleaned.aws_region_name;
                    }
                    const endpoint = this._coerceString(dataset.aws_endpoint_url);
                    if (endpoint) {
                        cleaned.aws_endpoint_url = endpoint;
                    } else {
                        delete cleaned.aws_endpoint_url;
                    }
                    const accessKey = this._coerceString(dataset.aws_access_key_id);
                    if (accessKey) {
                        cleaned.aws_access_key_id = accessKey;
                    } else {
                        delete cleaned.aws_access_key_id;
                    }
                    const secretKey = this._coerceString(dataset.aws_secret_access_key);
                    if (secretKey) {
                        cleaned.aws_secret_access_key = secretKey;
                    } else {
                        delete cleaned.aws_secret_access_key;
                    }
                } else {
                    delete cleaned.aws_bucket_name;
                    delete cleaned.aws_data_prefix;
                    delete cleaned.aws_region_name;
                    delete cleaned.aws_endpoint_url;
                    delete cleaned.aws_access_key_id;
                    delete cleaned.aws_secret_access_key;
                }

                if (dataset.parquet && typeof dataset.parquet === 'object' && !Array.isArray(dataset.parquet)) {
                    const parquet = {};
                    const assignString = (key) => {
                        const value = this._coerceString(dataset.parquet[key]);
                        if (value) {
                            parquet[key] = value;
                        }
                    };
                    assignString('path');
                    assignString('filename_column');
                    assignString('caption_column');
                    assignString('fallback_caption_column');
                    assignString('width_column');
                    assignString('height_column');
                    if (dataset.parquet.identifier_includes_extension !== undefined) {
                        parquet.identifier_includes_extension = this.normalizeBoolean(dataset.parquet.identifier_includes_extension);
                    }
                    if (Object.keys(parquet).length) {
                        cleaned.parquet = parquet;
                    } else {
                        delete cleaned.parquet;
                    }
                } else {
                    delete cleaned.parquet;
                }

                if (cleaned.type === 'aws') {
                    const assignAws = (key) => {
                        const value = this._coerceString(dataset[key]);
                        if (value) {
                            cleaned[key] = value;
                        } else {
                            delete cleaned[key];
                        }
                    };
                    assignAws('aws_bucket_name');
                    assignAws('aws_data_prefix');
                    assignAws('aws_region_name');
                    assignAws('aws_endpoint_url');
                    assignAws('aws_access_key_id');
                    assignAws('aws_secret_access_key');
                } else {
                    delete cleaned.aws_bucket_name;
                    delete cleaned.aws_data_prefix;
                    delete cleaned.aws_region_name;
                    delete cleaned.aws_endpoint_url;
                    delete cleaned.aws_access_key_id;
                    delete cleaned.aws_secret_access_key;
                }

                if (cleaned.type === 'csv') {
                    const csvFile = this._coerceString(dataset.csv_file);
                    if (csvFile) {
                        cleaned.csv_file = csvFile;
                    } else {
                        delete cleaned.csv_file;
                    }
                    const csvCaption = this._coerceString(dataset.csv_caption_column);
                    if (csvCaption) {
                        cleaned.csv_caption_column = csvCaption;
                    } else {
                        delete cleaned.csv_caption_column;
                    }
                    const csvCache = this._coerceString(dataset.csv_cache_dir);
                    if (csvCache) {
                        cleaned.csv_cache_dir = csvCache;
                    } else {
                        delete cleaned.csv_cache_dir;
                    }
                } else {
                    delete cleaned.csv_file;
                    delete cleaned.csv_caption_column;
                    delete cleaned.csv_cache_dir;
                }

                if (cleaned.type === 'huggingface') {
                    const assignHf = (key) => {
                        const value = this._coerceString(dataset[key]);
                        if (value) {
                            cleaned[key] = value;
                        } else {
                            delete cleaned[key];
                        }
                    };
                    assignHf('dataset_name');
                    assignHf('dataset_config');
                    assignHf('split');
                    assignHf('revision');
                    assignHf('image_column');
                    assignHf('caption_column');

                    delete cleaned.streaming;

                    const authToken = this._coerceString(dataset.auth_token);
                    if (authToken) {
                        cleaned.auth_token = authToken;
                    } else {
                        delete cleaned.auth_token;
                    }
                } else {
                    delete cleaned.dataset_name;
                    delete cleaned.dataset_config;
                    delete cleaned.split;
                    delete cleaned.revision;
                    delete cleaned.image_column;
                    delete cleaned.caption_column;
                    delete cleaned.streaming;
                    delete cleaned.auth_token;
                }

                return cleaned;
            };

            const sanitized = source.map((dataset) => sanitizeDataset(dataset));
            this.datasetValidationErrors = validationErrors;
            return sanitized;
        },
        sanitizeValueForSave(value) {
            if (Array.isArray(value)) {
                return value.map((item) => this.sanitizeValueForSave(item));
            }
            if (!value || typeof value !== 'object') {
                return value;
            }

            const cleaned = {};
            Object.entries(value).forEach(([key, entryValue]) => {
                if (key.startsWith('_')) {
                    return;
                }
                cleaned[key] = this.sanitizeValueForSave(entryValue);
            });
            return cleaned;
        },
        refreshDatasetsJson() {
            const sanitized = this.prepareDatasetsForSave();
            this.dataLoaderJson = JSON.stringify(sanitized, null, 2);
        },
        markDatasetsDirty(options = {}) {
            const { refresh = true } = options || {};
            this.hasUnsavedChanges = true;
            if (refresh && this.dataLoaderMode !== 'json') {
                this.refreshDatasetsJson();
            }
        },
        async loadCaptionFilters(force = false) {
            if (!force && Array.isArray(this.captionFilters) && this.captionFilters.length > 0) {
                return this.captionFilters;
            }
            if (this.captionFiltersLoading && this._captionFiltersPromise) {
                return this._captionFiltersPromise;
            }
            this.captionFiltersLoading = true;
            this._captionFiltersPromise = (async () => {
                try {
                    const response = await fetch('/api/caption-filters');
                    if (!response.ok) {
                        throw new Error('Failed to load caption filters');
                    }
                    const data = await response.json();
                    this.captionFilters = (data.filters || []).map((filter) => ({
                        name: filter.name,
                        label: filter.label || filter.name,
                        description: filter.description || '',
                        entries: filter.entries || [],
                        path: filter.path || filter.relative_path || '',
                        created_at: filter.created_at || null,
                        updated_at: filter.updated_at || null
                    }));
                    window.dispatchEvent(new CustomEvent('caption-filters-updated', {
                        detail: { filters: this.captionFilters }
                    }));
                } catch (error) {
                    console.error('Failed to load caption filters:', error);
                    if (force) {
                        window.showToast('Failed to load caption filters', 'error');
                    }
                    this.captionFilters = [];
                } finally {
                    this.captionFiltersLoading = false;
                }
                return this.captionFilters;
            })();
            try {
                return await this._captionFiltersPromise;
            } finally {
                this._captionFiltersPromise = null;
            }
        },
        resolveCaptionFilterValue(value) {
            if (!value) {
                return value;
            }
            const filters = Array.isArray(this.captionFilters) ? this.captionFilters : [];
            for (const filter of filters) {
                if (!filter) {
                    continue;
                }
                const canonical = filter.path || filter.name;
                if (!canonical) {
                    continue;
                }
                if (value === canonical || value === filter.name) {
                    return canonical;
                }
            }
            return value;
        },
        getConditioningDatasetIds(excludeId = null) {
            if (!Array.isArray(this.datasets)) {
                return [];
            }
            return this.datasets
                .filter((dataset) => dataset && dataset.dataset_type === 'conditioning')
                .map((dataset) => dataset.id)
                .filter((id) => id && id !== excludeId);
        },
        addConditioningGenerator(dataset) {
            if (!dataset) {
                return;
            }
            if (Array.isArray(dataset.conditioning_data) && dataset.conditioning_data.length > 0) {
                dataset.conditioning_data = [];
                window.showToast('Cleared linked conditioning datasets; generators take precedence.', 'info');
            }
            if (!Array.isArray(dataset.conditioning)) {
                dataset.conditioning = [];
            }
            const entry = this.normalizeConditioningEntry({ type: '', params: {} }, dataset.id, dataset.conditioning.length);
            dataset.conditioning.push(entry);
            this.markDatasetsDirty();
            window.showToast('Added conditioning generator (unsaved)', 'info');
        },
        removeConditioningGenerator(dataset, entryKey) {
            if (!dataset || !Array.isArray(dataset.conditioning)) {
                return;
            }
            const previousLength = dataset.conditioning.length;
            dataset.conditioning = dataset.conditioning.filter((entry) => {
                if (!entry) {
                    return false;
                }
                const key = entry._uiKey || entry.id;
                return key !== entryKey;
            });
            if (dataset.conditioning.length !== previousLength) {
                this.markDatasetsDirty();
                window.showToast('Removed conditioning generator (unsaved)', 'info');
            }
        },
        updateConditioningParams(entry, text) {
            if (!entry) {
                return;
            }
            entry._paramsText = text;
            const parsed = this.safeParseJSON(text);
            if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
                entry.params = parsed;
                entry._paramsParseError = null;
            } else {
                entry._paramsParseError = 'Invalid JSON';
            }
            this.markDatasetsDirty();
        },
        setConditioningCaptionsMode(entry, mode) {
            if (!entry) {
                return;
            }
            entry._captionsMode = mode;
            if (mode === 'none') {
                entry._captionsValue = '';
                entry.captions = false;
            } else if (mode === 'single') {
                if (!entry._captionsValue) {
                    entry._captionsValue = '';
                }
                entry.captions = entry._captionsValue.trim() || undefined;
            } else if (mode === 'list') {
                if (!entry._captionsValue) {
                    entry._captionsValue = '';
                }
                const items = entry._captionsValue
                    .split(/\r?\n/)
                    .map((line) => line.trim())
                    .filter((line) => line !== '');
                entry.captions = items.length ? items : undefined;
            } else {
                entry._captionsValue = '';
                delete entry.captions;
            }
            this.markDatasetsDirty();
        },
        updateConditioningCaptionsValue(entry, value) {
            if (!entry) {
                return;
            }
            entry._captionsValue = value;
            this.setConditioningCaptionsMode(entry, entry._captionsMode || 'inherit');
        },
        async loadDatasetsAfterEnvironmentChange() {
             try {
                 if (!this.activeEnvironmentConfig) {
                     await this.fetchActiveEnvironmentConfig();
                 }

                 const normalized = this.buildNormalizedConfig(this.activeEnvironmentConfig);
                 const backendPath = this.firstNonEmpty(
                     normalized.data_backend_config,
                     normalized.dataloader_config,
                     normalized.multidatabackend,
                 );

                 if (backendPath) {
                     await this.loadDatasetsFromConfig(backendPath);
                 } else {
                     await this.loadDatasets();
                 }
             } catch (error) {
                 console.error('Error loading datasets after environment change:', error);
                 await this.loadDatasets();
             }
        },
         // Dataset management
        datasets: [],
        knownConditioningGenerators: [
            'canny', 'edges', 'hed', 'lineart', 'lineart_coarse', 'scribble',
            'depth', 'depth_midas', 'depth_leres', 'normal_map', 'random_masks',
            'binary_mask', 'inpainting', 'jpeg_artifacts', 'superresolution',
            'content_shuffle', 'tile', 'rembg'
        ],
        captionFilters: [],
        captionFiltersLoading: false,
        _captionFiltersPromise: null,
         datasetBlueprints: [],
         datasetsLoading: false,
         _datasetsReloadInFlight: null,
         dataLoaderMode: 'builder', // 'builder' or 'json'
         dataLoaderJson: '',
         hasUnsavedChanges: false,
         // Save dialog state
         showSaveDialog: false,
         createBackupOption: false,
         saveDialogResolve: null,
         // Validation errors
         datasetValidationErrors: {},
         eventDockMinHeight: 160,
         eventDockMaxRatio: 0.75,
         eventDockCollapsedHeight: 56,
         eventDockHeight: (() => {
             const stored = parseInt(localStorage.getItem('eventDockHeight'), 10);
             if (Number.isFinite(stored) && stored >= 160) {
                 return stored;
             }
             return 280;
         })(),
         eventDockCollapsed: (() => {
             const stored = localStorage.getItem('eventDockCollapsed');
             return stored === 'true';
         })(),
         eventDockIsResizing: false,
         toggleEventDock() {
             this.eventDockCollapsed = !this.eventDockCollapsed;
             localStorage.setItem('eventDockCollapsed', this.eventDockCollapsed);
             if (!this.eventDockCollapsed) {
                 this.ensureEventDockWithinBounds();
             }
         },
         ensureEventDockWithinBounds() {
             const maxHeight = Math.max(this.eventDockMinHeight, Math.floor(window.innerHeight * this.eventDockMaxRatio));
             if (this.eventDockHeight < this.eventDockMinHeight) {
                 this.eventDockHeight = this.eventDockMinHeight;
                 localStorage.setItem('eventDockHeight', this.eventDockHeight);
             } else if (this.eventDockHeight > maxHeight) {
                 this.eventDockHeight = maxHeight;
                 localStorage.setItem('eventDockHeight', this.eventDockHeight);
             }
         },
         setEventDockHeight(height) {
             const maxHeight = Math.max(this.eventDockMinHeight, Math.floor(window.innerHeight * this.eventDockMaxRatio));
             const clamped = Math.max(this.eventDockMinHeight, Math.min(height, maxHeight));
             this.eventDockHeight = Math.round(clamped);
             localStorage.setItem('eventDockHeight', this.eventDockHeight);
         },
         startEventDockResize(event) {
             if (this.eventDockCollapsed) return;
             event.preventDefault();
             this.eventDockIsResizing = true;
             const startY = event.clientY;
             const startHeight = this.eventDockHeight;
             const onMove = (moveEvt) => {
                 const delta = startY - moveEvt.clientY;
                 this.setEventDockHeight(startHeight + delta);
             };
             const onUp = () => {
                 this.eventDockIsResizing = false;
                 window.removeEventListener('pointermove', onMove);
                 window.removeEventListener('pointerup', onUp);
             };
             window.addEventListener('pointermove', onMove);
             window.addEventListener('pointerup', onUp);
         },
        get eventDockStyle() {
            return this.eventDockCollapsed
                ? `height: ${this.eventDockCollapsedHeight}px;`
                : `height: ${this.eventDockHeight}px;`;
        },
        get eventDockPaddingStyle() {
            return this.eventDockCollapsed
                ? 'padding-bottom: 4rem;'
                : `padding-bottom: calc(${this.eventDockHeight}px + 3rem);`;
        },
        systemStatus: null,
        systemStatusLoading: false,
        systemStatusError: null,
        _systemStatusTimer: null,
        _beforeUnloadHandler: null,
        _visibilityChangeHandler: null,
        async refreshSystemStatus() {
            if (this.systemStatusLoading) {
                return;
            }
            this.systemStatusLoading = true;
            try {
                const response = await fetch('/api/system/status', { cache: 'no-store' });
                if (!response.ok) {
                    throw new Error(`Request failed with status ${response.status}`);
                }
                const payload = await response.json();
                this.systemStatus = this.normalizeSystemStatus(payload);
                this.systemStatusError = null;
            } catch (error) {
                console.error('Failed to load system status:', error);
                if (!this.systemStatus) {
                    this.systemStatusError = 'Stats unavailable';
                }
            } finally {
                this.systemStatusLoading = false;
            }
        },
        normalizeSystemStatus(payload) {
            const data = payload || {};
            const loadValue =
                typeof data.load_avg_5min === 'number' && Number.isFinite(data.load_avg_5min)
                    ? Number(data.load_avg_5min)
                    : null;
            const memoryValue =
                typeof data.memory_percent === 'number' && Number.isFinite(data.memory_percent)
                    ? Number(data.memory_percent)
                    : null;
            const gpus = Array.isArray(data.gpus)
                ? data.gpus.map((gpu, index) => {
                      const idx = typeof gpu?.index === 'number' ? gpu.index : index;
                      const name =
                          typeof gpu?.name === 'string' && gpu.name.trim()
                              ? gpu.name.trim()
                              : `GPU ${idx}`;
                      const utilization =
                          typeof gpu?.utilization_percent === 'number' && Number.isFinite(gpu.utilization_percent)
                              ? Number(gpu.utilization_percent)
                              : null;
                      const memoryPercent =
                          typeof gpu?.memory_percent === 'number' && Number.isFinite(gpu.memory_percent)
                              ? Number(gpu.memory_percent)
                              : null;
                      return {
                          index: idx,
                          name,
                          backend: gpu?.backend || data.backend || null,
                          utilization_percent: utilization,
                          memory_percent: memoryPercent,
                      };
                  })
                : [];
            const offloadRaw = data.deepspeed_offload;
            const offload = offloadRaw && typeof offloadRaw === 'object'
                ? {
                      path: typeof offloadRaw.path === 'string' ? offloadRaw.path : null,
                      size_bytes:
                          typeof offloadRaw.size_bytes === 'number' && Number.isFinite(offloadRaw.size_bytes)
                              ? offloadRaw.size_bytes
                              : null,
                      file_count:
                          typeof offloadRaw.file_count === 'number' && Number.isFinite(offloadRaw.file_count)
                              ? offloadRaw.file_count
                              : null,
                      device: typeof offloadRaw.device === 'string' ? offloadRaw.device : null,
                      stage:
                          typeof offloadRaw.stage === 'number' && Number.isFinite(offloadRaw.stage)
                              ? offloadRaw.stage
                              : null,
                  }
                : null;
            return {
                load_avg_5min: loadValue,
                memory_percent: memoryValue,
                gpus,
                deepspeed_offload: offload,
                timestamp: typeof data.timestamp === 'number' && Number.isFinite(data.timestamp) ? data.timestamp : null,
            };
        },
        formatBytes(bytes, precision = 1) {
            const value = typeof bytes === 'number' && Number.isFinite(bytes) ? bytes : null;
            if (value === null || value < 0) {
                return '—';
            }
            if (value === 0) {
                return '0 B';
            }
            const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
            const exponent = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
            const scaled = value / (1024 ** exponent);
            const formatted = scaled >= 100 ? Math.round(scaled) : scaled.toFixed(precision);
            return `${formatted} ${units[exponent]}`;
        },
        isDocumentVisible() {
            return (
                typeof document === 'undefined' ||
                typeof document.visibilityState === 'undefined' ||
                document.visibilityState === 'visible'
            );
        },
        get systemStatusItems() {
            if (!this.systemStatus) {
                return [];
            }
            const items = [];
            const load = this.systemStatus.load_avg_5min;
            items.push({
                key: 'load',
                label: 'Load 5m',
                value: load,
                formatted: load !== null ? load.toFixed(2) : '—',
                tooltip: '5 minute system load average',
            });
            const memory = this.systemStatus.memory_percent;
            items.push({
                key: 'memory',
                label: 'Mem',
                value: memory,
                formatted: memory !== null ? `${Math.round(memory)}%` : '—',
                tooltip: 'System memory usage',
            });
            const gpus = Array.isArray(this.systemStatus.gpus) ? this.systemStatus.gpus : [];
            gpus.forEach((gpu, index) => {
                const util = gpu.utilization_percent;
                const mem = gpu.memory_percent;
                const trimmedName = typeof gpu.name === 'string' ? gpu.name.trim() : '';
                const displayLabel =
                    trimmedName ||
                    (typeof gpu.index === 'number' ? `GPU${gpu.index}` : `GPU${index}`);
                const valueParts = [];
                if (util !== null) {
                    valueParts.push(`${Math.round(util)}%`);
                }
                if (mem !== null) {
                    valueParts.push(`${Math.round(mem)}% mem`);
                }
                const formattedDisplay = valueParts.length > 0 ? valueParts.join(' · ') : '—';
                const tooltipParts = [];
                if (trimmedName) {
                    tooltipParts.push(trimmedName);
                } else {
                    tooltipParts.push(displayLabel);
                }
                if (util !== null) {
                    tooltipParts.push(`Util: ${util.toFixed(1)}%`);
                }
                if (mem !== null) {
                    tooltipParts.push(`Mem: ${mem.toFixed(1)}%`);
                }
                items.push({
                    key: `gpu-${gpu.index ?? index}`,
                    label: displayLabel,
                    value: util,
                    memory: mem,
                    formatted: formattedDisplay,
                    tooltip: tooltipParts.join(' • '),
                });
            });
            const offloadInfo = this.systemStatus.deepspeed_offload;
            if (offloadInfo && offloadInfo.size_bytes !== null) {
                const deviceLabel = offloadInfo.device
                    ? offloadInfo.device.toUpperCase()
                    : 'Offload';
                const tooltipParts = [];
                if (offloadInfo.path) {
                    tooltipParts.push(`Path: ${offloadInfo.path}`);
                }
                if (offloadInfo.stage !== null) {
                    tooltipParts.push(`ZeRO stage ${offloadInfo.stage}`);
                }
                if (offloadInfo.file_count !== null) {
                    tooltipParts.push(`${offloadInfo.file_count} files`);
                }
                const tooltip = tooltipParts.join(' • ') || 'DeepSpeed offload directory usage';
                items.push({
                    key: 'deepspeed-offload',
                    label: deviceLabel === 'OFFLOAD' ? 'Offload' : deviceLabel,
                    value: offloadInfo.size_bytes,
                    formatted: this.formatBytes(offloadInfo.size_bytes),
                    tooltip,
                });
            }
            return items;
        },
        get hasSystemStatus() {
            return this.systemStatusItems.length > 0;
        },
        async loadDatasets() {
             this.datasetsLoading = true;
             try {
                 // Load dataset plan
                 const planResponse = await fetch('/api/datasets/plan');
                if (planResponse.ok) {
                    const planData = await planResponse.json();
                    const datasetList = Array.isArray(planData.datasets) ? planData.datasets : [];
                    this.datasets = this.normalizeDatasets(datasetList);
                    const sanitized = this.prepareDatasetsForSave(this.datasets);
                    this.dataLoaderJson = JSON.stringify(sanitized, null, 2);
                    this.hasUnsavedChanges = false;
                    this.datasetValidationErrors = {};
                    if (Alpine.store('trainer')) {
                        Alpine.store('trainer').datasets = this.datasets;
                        Alpine.store('trainer').dataLoaderJson = this.dataLoaderJson;
                    }
                    if (window.dataloaderSectionComponentInstance &&
                        typeof window.dataloaderSectionComponentInstance.syncGeneratorSelectionsForAll === 'function') {
                        window.dataloaderSectionComponentInstance.syncGeneratorSelectionsForAll();
                    } else {
                        window.__pendingGeneratorSync = true;
                    }
                }

                 // Load blueprints for presets
                 const blueprintResponse = await fetch('/api/datasets/blueprints');
                 if (blueprintResponse.ok) {
                     const blueprintData = await blueprintResponse.json();
                     this.datasetBlueprints = blueprintData.blueprints || [];
                 }
            } catch (error) {
                console.error('Error loading datasets:', error);
                window.showToast('Failed to load dataset configuration', 'error');
                this.datasets = [];
                this.dataLoaderJson = JSON.stringify([], null, 2);
                this.hasUnsavedChanges = false;
            } finally {
                this.datasetsLoading = false;
            }
        },
         async loadDatasetsFromConfig(dataBackendConfigPath) {
             this.datasetsLoading = true;
             try {
                 // Construct the URL to fetch the dataset configuration file
                 // The path might be relative or absolute, so we need to handle it properly
                 let fetchUrl;
                if (dataBackendConfigPath.startsWith('http://') || dataBackendConfigPath.startsWith('https://')) {
                    // Full URL
                    fetchUrl = dataBackendConfigPath;
                } else {
                    // Local filesystem path (absolute or relative) should always be resolved via backend API
                    fetchUrl = `/api/configs/data-backend-file?path=${encodeURIComponent(dataBackendConfigPath)}`;
                }

                 const response = await fetch(fetchUrl);
                 if (response.ok) {
                     const datasets = await response.json();

                     // Validate that we got an array
                     if (Array.isArray(datasets)) {
                        this.datasets = this.normalizeDatasets(datasets);
                        const sanitized = this.prepareDatasetsForSave(this.datasets);
                        this.dataLoaderJson = JSON.stringify(sanitized, null, 2);

                        // Update the trainer store reference
                        if (Alpine.store('trainer')) {
                            Alpine.store('trainer').datasets = this.datasets;
                            Alpine.store('trainer').dataLoaderJson = this.dataLoaderJson;
                        }

                        this.hasUnsavedChanges = false;
                        this.datasetValidationErrors = {};
                        if (window.dataloaderSectionComponentInstance &&
                            typeof window.dataloaderSectionComponentInstance.syncGeneratorSelectionsForAll === 'function') {
                            window.dataloaderSectionComponentInstance.syncGeneratorSelectionsForAll();
                        } else {
                            window.__pendingGeneratorSync = true;
                        }
                        window.showToast('Dataset configuration loaded from environment', 'success');
                    } else {
                         throw new Error('Invalid dataset configuration format - expected an array');
                     }
                 } else {
                     throw new Error(`Failed to load dataset configuration from ${dataBackendConfigPath}`);
                 }
             } catch (error) {
                console.error('Error loading dataset configuration:', error);
                window.showToast(`Failed to load dataset configuration: ${error.message}`, 'error');
                // Clear datasets on error
               this.datasets = [];
               this.dataLoaderJson = JSON.stringify([], null, 2);
                this.hasUnsavedChanges = false;
            } finally {
               this.datasetsLoading = false;
            }
        },
        async loadDatasetsAfterEnvironmentChange() {
            if (!this._datasetsReloadInFlight) {
                this.datasetsLoading = true;
                this.datasets = [];
                this.dataLoaderJson = JSON.stringify([], null, 2);
                this.hasUnsavedChanges = false;
            }

            if (this._datasetsReloadInFlight) {
                return this._datasetsReloadInFlight;
            }

            this._datasetsReloadInFlight = (async () => {
                try {
                    if (!this.activeEnvironmentConfig) {
                        await this.fetchActiveEnvironmentConfig();
                    }

                    const normalized = this.buildNormalizedConfig(this.activeEnvironmentConfig);
                    const backendPath = this.firstNonEmpty(
                        normalized.data_backend_config,
                        normalized.dataloader_config,
                        normalized.multidatabackend,
                    );

                    if (backendPath) {
                        await this.loadDatasetsFromConfig(backendPath);
                    } else {
                        await this.loadDatasets();
                    }
                } catch (error) {
                    console.error('Error loading datasets after environment change:', error);
                    await this.loadDatasets();
                }
            })();

            try {
                await this._datasetsReloadInFlight;
            } finally {
                this._datasetsReloadInFlight = null;
             }
         },
        async addDataset(type) {
            try {
                const dataset = {
                    id: `dataset-${Date.now()}`,
                    type: 'local',
                    dataset_type: type,
                    repeats: 0,
                    probability: 1,
                    minimum_aspect_ratio: null,
                    maximum_aspect_ratio: null,
                    crop_aspect_buckets: [],
                    _cropAspectBucketsText: '',
                    skip_file_discovery: '',
                    hash_filenames: true,
                    preserve_data_backend_cache: false,
                    is_regularisation_data: false,
                    prepend_instance_prompt: false,
                    only_instance_prompt: false,
                    instance_prompt: '',
                    cache_dir_vae: '',
                    caption_filter_list: '',
                    _selectedCaptionFilter: '',
                    text_embeds: '',
                    image_embeds: '',
                    parquet: {},
                    aws_bucket_name: '',
                    aws_data_prefix: '',
                    aws_region_name: '',
                    aws_endpoint_url: '',
                    aws_access_key_id: '',
                    aws_secret_access_key: '',
                    _awsAccessVisible: false,
                    _awsSecretVisible: false,
                    csv_file: '',
                    csv_caption_column: '',
                    csv_cache_dir: '',
                    dataset_name: '',
                    dataset_config: '',
                    split: '',
                    revision: '',
                    image_column: '',
                    caption_column: '',
                    streaming: false,
                    auth_token: '',
                    disabled: false,
                    _showAdvanced: false,
                    _connectionStatus: null,
                    _connectionMessage: '',
                    _connectionDetails: null,
                    _connectionTesting: false
                };

                // Use cache_dir for embeds, instance_data_dir and resolution for others
                if (type === 'text_embeds' || type === 'image_embeds') {
                    dataset.cache_dir = '';
                    dataset.write_batch_size = 128;
                } else {
                    dataset.instance_data_dir = '';
                    dataset.resolution = 1024;
                    dataset.resolution_type = 'pixel';
                }

                const normalized = this.normalizeDataset(dataset, this.datasets.length);
                this.datasets.push(normalized);
                this.hasUnsavedChanges = true;
                this.refreshDatasetsJson();
                window.showToast(`Added ${type} dataset (unsaved)`, 'info');
            } catch (error) {
                console.error('Error adding dataset:', error);
                window.showToast('Failed to add dataset', 'error');
            }
        },
        async duplicateDataset(datasetId) {
            try {
                const index = this.datasets.findIndex(d => d.id === datasetId);
                 if (index === -1) {
                     window.showToast('Dataset not found', 'error');
                     return;
                 }

                const clone = JSON.parse(JSON.stringify(this.datasets[index]));
                delete clone._uiKey;
                clone._showAdvanced = false;
                if (Array.isArray(clone.conditioning)) {
                    clone.conditioning.forEach((entry) => {
                        if (entry && typeof entry === 'object') {
                            delete entry._uiKey;
                        }
                    });
                }
                const baseId = clone.id || `dataset-${Date.now()}`;
                let candidate = `${baseId}-copy`;
                let suffix = 1;
                while (this.datasets.some(d => d.id === candidate)) {
                    candidate = `${baseId}-copy-${suffix++}`;
                }
                clone.id = candidate;

                const normalizedClone = this.normalizeDataset(clone, index + 1);
                normalizedClone._connectionStatus = null;
                normalizedClone._connectionMessage = '';
                normalizedClone._connectionDetails = null;
                normalizedClone._connectionTesting = false;
                this.datasets.splice(index + 1, 0, normalizedClone);
                this.hasUnsavedChanges = true;
                this.refreshDatasetsJson();
                window.showToast('Dataset duplicated (unsaved)', 'info');
            } catch (error) {
                console.error('Error duplicating dataset:', error);
                window.showToast('Failed to duplicate dataset', 'error');
            }
        },
        async removeDataset(datasetId) {
            if (!confirm('Are you sure you want to remove this dataset?')) {
                return; // User cancelled
            }

            try {
                this.datasets = this.datasets.filter(d => d.id !== datasetId);
                this.hasUnsavedChanges = true;
                this.refreshDatasetsJson();
                window.showToast('Dataset removed (unsaved)', 'info');
            } catch (error) {
                console.error('Error removing dataset:', error);
                window.showToast('Failed to remove dataset', 'error');
            }
        },
        async saveDatasets({ showToast = true, createBackup = false, skipConfirmation = false } = {}) {
            // Show confirmation dialog unless skipped
            if (!skipConfirmation) {
                // Show the save dialog and wait for user response
                this.showSaveDialog = true;
                 this.createBackupOption = false; // Reset checkbox state

                 // Return a promise that will be resolved by the dialog buttons
                 return new Promise((resolve) => {
                     this.saveDialogResolve = resolve;
                 });
             }

            // Perform the actual save
            try {
                const payloadDatasets = this.prepareDatasetsForSave();
                this.datasetValidationErrors = {};
                const response = await fetch('/api/datasets/plan', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        datasets: payloadDatasets,
                        createBackup: createBackup
                    })
                });

                if (response.ok) {
                    this.hasUnsavedChanges = false;
                    this.datasetValidationErrors = {}; // Clear validation errors
                    const result = await response.json();
                    if (showToast) {
                        let message = 'Dataset configuration saved';
                        if (result.backupPath) {
                            message += ` (backup: ${result.backupPath})`;
                        }
                        window.showToast(message, 'success');
                    }
                    this.refreshDatasetsJson();
                } else {
                    const error = await response.json();

                     // Handle validation errors specifically
                     if (response.status === 422 && error.detail?.validations) {
                         const validationErrors = {};
                         let errorMessages = [];

                         error.detail.validations.forEach(v => {
                             if (v.field) {
                                 // Store field-specific errors
                                 validationErrors[v.field] = v.message;
                                 // Extract dataset ID from field name (e.g., "dataset-123.dataset_type")
                                 const parts = v.field.split('.');
                                 if (parts.length > 1) {
                                     errorMessages.push(`${parts[0]}: ${v.message}`);
                                 } else {
                                     errorMessages.push(v.message);
                                 }
                             }
                         });

                         this.datasetValidationErrors = validationErrors;

                         // Show informative error message
                         const errorCount = errorMessages.length;
                         if (errorCount > 0) {
                             window.showToast(`Validation failed (${errorCount} error${errorCount > 1 ? 's' : ''}): ${errorMessages.join(', ')}`, 'error');
                         } else {
                             window.showToast('Validation failed. Please check required fields.', 'error');
                         }
                     } else {
                         window.showToast(error.detail?.message || 'Failed to save datasets', 'error');
                     }
                 }
            } catch (error) {
                console.error('Error saving datasets:', error);
                window.showToast('Failed to save dataset configuration', 'error');
            }
        },
        async saveJsonDatasets() {
            try {
                // Parse JSON to validate it
                const parsedDatasets = JSON.parse(this.dataLoaderJson);

                // Update datasets array
                this.datasets = this.normalizeDatasets(parsedDatasets);
                this.hasUnsavedChanges = true;
                this.refreshDatasetsJson();

                // Call regular save with confirmation
                await this.saveDatasets({ showToast: true, skipConfirmation: true });
            } catch (e) {
                window.showToast('Invalid JSON format. Please fix the JSON and try again.', 'error');
            }
        },
        switchDataLoaderMode(mode) {
            this.dataLoaderMode = mode;
            if (mode === 'json') {
                this.refreshDatasetsJson();
            } else if (mode === 'builder') {
                try {
                    const parsedDatasets = JSON.parse(this.dataLoaderJson);
                    this.datasets = this.normalizeDatasets(parsedDatasets);
                    // Mark as having unsaved changes when successfully importing from JSON
                    this.hasUnsavedChanges = true;
                    this.refreshDatasetsJson();
                } catch (e) {
                    window.showToast('Invalid JSON format', 'error');
                }
            }
        },
        async initializeWebUIState() {
            try {
                const response = await fetch('/api/webui/state');
                if (!response.ok) {
                    console.warn('Failed to load WebUI state');
                    return;
                }
               const data = await response.json();
               const onboarding = data.onboarding || {};
               const defaultsPayload = data.resolved_defaults || data.defaults || {};
               const normalizedDefaults = this.normalizeDefaultsPayload(defaultsPayload);
               this.defaults = normalizedDefaults;
               this.preserveDefaultsOption = normalizedDefaults.auto_preserve_defaults;
               this.onboardingSteps = onboarding.steps || [];
               this.overlayVisible = Boolean(onboarding.overlay_required);
                await this.setActiveOnboardingStep();
                if (typeof window.applyTheme === 'function' && normalizedDefaults.theme) {
                    window.applyTheme(normalizedDefaults.theme);
                }
                this.applyEventPreferences();
                window.dispatchEvent(new CustomEvent('webui-defaults-updated', {
                    detail: {
                        defaults: data.defaults || {},
                        resolved_defaults: normalizedDefaults,
                        source: 'initial-load'
                    }
                }));
            } catch (error) {
                console.error('Failed to load WebUI state', error);
            }
        },
        async ensureGpuInventory() {
            if (this.gpuInventory.loading) {
                return;
            }
            if (this.gpuInventory.devices.length || this.gpuInventory.detected) {
                return;
            }
            this.gpuInventory.loading = true;
            this.gpuInventory.error = '';
            try {
                const response = await fetch('/api/hardware/gpus');
                if (!response.ok) {
                    throw new Error(`GPU inventory request failed with status ${response.status}`);
                }
                const payload = await response.json();
                const devices = Array.isArray(payload.devices) ? payload.devices : [];
                this.gpuInventory.devices = devices.map((device, index) => ({
                    index: typeof device.index === 'number' ? device.index : index,
                    id: device.id ?? device.index ?? index,
                    name: device.name || `GPU ${typeof device.index === 'number' ? device.index : index}`,
                    memory_gb: typeof device.memory_gb === 'number' ? device.memory_gb : null,
                }));
                this.gpuInventory.backend = payload.backend || null;
                this.gpuInventory.count = typeof payload.count === 'number' ? payload.count : this.gpuInventory.devices.length;
                this.gpuInventory.detected = Boolean(payload.detected && this.gpuInventory.devices.length > 0);
                let optimal = typeof payload.optimal_processes === 'number' ? payload.optimal_processes : null;
                if (!optimal || optimal < 1) {
                    optimal = this.gpuInventory.devices.length || 1;
                }
                this.gpuInventory.optimalProcesses = optimal;
            } catch (error) {
                console.error('Failed to detect GPU inventory', error);
                this.gpuInventory.error = 'Unable to detect GPUs automatically. Configure processes manually if needed.';
                this.gpuInventory.devices = [];
                this.gpuInventory.backend = null;
                this.gpuInventory.count = 0;
                this.gpuInventory.detected = false;
                this.gpuInventory.optimalProcesses = 1;
            } finally {
                this.gpuInventory.loading = false;
            }
        },
        normalizeAccelerateSelection(rawValue) {
            const fallback = {
                mode: 'auto',
                manual_count: null,
                device_ids: [],
                '--num_processes': null,
            };
            if (!rawValue) {
                return { ...fallback };
            }
            let payload = rawValue;
            if (typeof rawValue === 'string') {
                const trimmed = rawValue.trim();
                if (!trimmed) {
                    return { ...fallback };
                }
                try {
                    payload = JSON.parse(trimmed);
                } catch (error) {
                    const parsed = parseInt(trimmed, 10);
                    if (Number.isFinite(parsed)) {
                        return {
                            mode: parsed > 1 ? 'manual' : 'disabled',
                            manual_count: parsed,
                            device_ids: [],
                            '--num_processes': parsed,
                        };
                    }
                    return { ...fallback };
                }
            }
            if (!payload || typeof payload !== 'object') {
                return { ...fallback };
            }
            const clone = { ...payload };
            const rawDevices = Array.isArray(clone.device_ids) ? clone.device_ids : [];
            const deviceIds = rawDevices
                .map((value) => {
                    const parsed = parseInt(value, 10);
                    return Number.isNaN(parsed) ? null : parsed;
                })
                .filter((value) => value !== null);
            const manualCount = typeof clone.manual_count === 'number'
                ? clone.manual_count
                : parseInt(clone.manual_count, 10);
            const numProcesses = typeof clone['--num_processes'] === 'number'
                ? clone['--num_processes']
                : parseInt(clone['--num_processes'], 10);
            const validModes = ['auto', 'manual', 'disabled', 'hardware'];
            let mode = typeof clone.mode === 'string' ? clone.mode.trim().toLowerCase() : undefined;
            if (!validModes.includes(mode || '')) {
                if (deviceIds.length > 1) {
                    mode = 'manual';
                } else if (Number.isFinite(numProcesses) && numProcesses > 1) {
                    mode = 'manual';
                } else if (Number.isFinite(numProcesses) && numProcesses <= 1) {
                    mode = 'disabled';
                } else {
                    mode = 'auto';
                }
            }
            const normalizedMode = validModes.includes(mode || '') ? mode : 'auto';
            return {
                mode: normalizedMode || 'auto',
                manual_count:
                    normalizedMode === 'manual' && Number.isFinite(manualCount) && manualCount > 0
                        ? manualCount
                        : null,
                device_ids: deviceIds,
                '--num_processes':
                    normalizedMode === 'hardware'
                        ? null
                        : Number.isFinite(numProcesses) && numProcesses > 0
                            ? numProcesses
                            : null,
            };
        },
        async prepareAccelerateOnboarding(rawValue) {
            await this.ensureGpuInventory();
            const normalized = this.normalizeAccelerateSelection(rawValue);
            const fallbackProcesses = this.gpuInventory.optimalProcesses || 1;
            const devices = Array.from(new Set(normalized.device_ids || []));
            let manualCount = normalized.manual_count || normalized['--num_processes'] || fallbackProcesses;
            if (!Number.isFinite(manualCount) || manualCount < 1) {
                manualCount = fallbackProcesses;
            }
            this.onboardingAccelerate = {
                mode: normalized.mode || 'auto',
                manualCount: Math.max(1, parseInt(manualCount, 10) || fallbackProcesses || 1),
                deviceIds: devices,
            };
            this.onboardingForm.value = normalized;
        },
        serializeAccelerateOnboardingValue() {
            const mode = (this.onboardingAccelerate.mode || 'auto').toLowerCase();
            const validModes = ['auto', 'manual', 'disabled', 'hardware'];
            const normalizedMode = validModes.includes(mode) ? mode : 'auto';
            const uniqueDeviceIds = Array.isArray(this.onboardingAccelerate.deviceIds)
                ? Array.from(new Set(this.onboardingAccelerate.deviceIds
                    .map((value) => {
                        const parsed = parseInt(value, 10);
                        return Number.isNaN(parsed) ? null : parsed;
                    })
                    .filter((value) => value !== null)))
                : [];
            const manualCount = parseInt(this.onboardingAccelerate.manualCount, 10);
            const hasManualCount = Number.isFinite(manualCount) && manualCount > 0;
            if (normalizedMode === 'hardware') {
                return { mode: 'hardware' };
            }
            let numProcesses;
            if (normalizedMode === 'disabled') {
                numProcesses = 1;
            } else if (normalizedMode === 'auto') {
                numProcesses = this.gpuInventory.optimalProcesses || this.gpuInventory.count || 1;
            } else if (normalizedMode === 'manual') {
                if (uniqueDeviceIds.length > 0) {
                    numProcesses = uniqueDeviceIds.length;
                } else if (hasManualCount) {
                    numProcesses = manualCount;
                } else {
                    this.overlayError = 'Select at least one GPU or provide a process count.';
                    return null;
                }
            } else {
                numProcesses = this.gpuInventory.optimalProcesses || 1;
            }

            const normalizedProcesses = Math.max(1, parseInt(numProcesses, 10) || 1);
            const payload = {
                mode: normalizedMode === 'manual' ? 'manual' : normalizedMode === 'disabled' ? 'disabled' : 'auto',
                '--num_processes': normalizedProcesses,
            };

            if (normalizedMode === 'manual' && uniqueDeviceIds.length > 0) {
                payload.device_ids = uniqueDeviceIds;
            } else if (normalizedMode === 'manual' && hasManualCount) {
                payload.manual_count = Math.max(1, manualCount);
            }

            return payload;
        },
        async setActiveOnboardingStep() {
            const nextStep = this.onboardingSteps.find(step => !step.is_complete && step.required);
            if (nextStep) {
                this.activeOnboardingStep = nextStep;
                const fallback = nextStep.applies_to_default && this.defaults
                    ? this.defaults[nextStep.applies_to_default]
                    : '';
                if (nextStep.input_type === 'accelerate_auto') {
                    await this.prepareAccelerateOnboarding(nextStep.value || fallback || null);
                } else if (nextStep.input_type === 'environment') {
                    this.onboardingForm.value = nextStep.value || '';
                } else {
                    this.onboardingForm.value = nextStep.value || fallback || '';
                }
                this.overlayVisible = true;
                this.overlayError = '';
                this.$nextTick(async () => {
                    if (this.$refs.onboardingInput && nextStep.input_type === 'directory') {
                        this.$refs.onboardingInput.focus();
                        this.$refs.onboardingInput.select();
                    }
                    if (nextStep.input_type === 'environment') {
                        const formEl = this.$refs.onboardingEnvironmentForm;
                        const component = formEl && formEl.__x ? formEl.__x.$data : null;
                        if (component && typeof component.prepareForDisplay === 'function') {
                            await component.prepareForDisplay();
                        }
                    }
                });
            } else {
                 this.activeOnboardingStep = null;
                 this.overlayVisible = false;
             }
         },
        async submitOnboardingStep() {
            if (!this.activeOnboardingStep) {
                return;
            }
            const completedStep = this.activeOnboardingStep;
            this.overlaySaving = true;
            this.overlayError = '';
            if (completedStep.input_type === 'accelerate_auto') {
                const payload = this.serializeAccelerateOnboardingValue();
                if (!payload) {
                    this.overlaySaving = false;
                    return;
                }
                this.onboardingForm.value = payload;
            }
            const currentInputValue = this.onboardingForm.value;
            try {
                const response = await fetch(`/api/webui/onboarding/steps/${this.activeOnboardingStep.id}`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ value: this.onboardingForm.value })
                 });
                 if (!response.ok) {
                     let detail = 'Failed to save preferences.';
                     try {
                         const errorPayload = await response.json();
                         detail = errorPayload.detail || detail;
                     } catch (_) {}
                     this.overlayError = detail;
                     return;
                 }
                const data = await response.json();
                const onboarding = data.onboarding || {};
                const defaultsPayload = data.resolved_defaults || data.defaults || {};
                const normalizedDefaults = this.normalizeDefaultsPayload(defaultsPayload);
                this.defaults = normalizedDefaults;
                this.preserveDefaultsOption = normalizedDefaults.auto_preserve_defaults;
                this.onboardingSteps = onboarding.steps || [];
                this.overlayVisible = Boolean(onboarding.overlay_required);
                const completedState = this.onboardingSteps.find(step => completedStep && step.id === completedStep.id);
                const normalizedValue = (completedState && completedState.value) || currentInputValue;
                if (completedStep && completedStep.applies_to_default === 'output_dir' && normalizedValue) {
                     const directField = document.querySelector('#output_dir');
                     const argField = Array.from(document.querySelectorAll('input')).find(input => input.name === '--output_dir');
                     const targetField = directField || argField;
                     if (targetField) {
                         targetField.value = normalizedValue;
                         targetField.dispatchEvent(new Event('input', { bubbles: true }));
                     }
                }
                if (window.showToast && completedStep.input_type !== 'environment') {
                    window.showToast('Saved web UI defaults.', 'success');
                }
                if (data) {
                    window.dispatchEvent(new CustomEvent('webui-defaults-updated', {
                        detail: {
                            defaults: data.defaults || {},
                            resolved_defaults: normalizedDefaults,
                            source: 'onboarding-step'
                        }
                    }));
                }
                await this.setActiveOnboardingStep();
            } catch (error) {
                console.error('Failed to save onboarding step', error);
                this.overlayError = 'Something went wrong. Please try again.';
            } finally {
                 this.overlaySaving = false;
            }
        },
        async handleOnboardingEnvironmentCreated(detail) {
            const payload = detail?.payload || {};
            const environment = detail?.environment || null;
            try {
                await this.loadEnvironmentConfigs();
            } catch (error) {
                console.error('Failed to reload environment configs after onboarding:', error);
            }
            const dataloaderStore = Alpine.store('dataloaderConfigs');
            if (dataloaderStore) {
                try {
                    await dataloaderStore.load(true);
                } catch (error) {
                    console.error('Failed to refresh dataloader configs after onboarding:', error);
                }
            }
            if (environment && typeof this.switchEnvironment === 'function') {
                try {
                    await this.switchEnvironment(environment.name);
                } catch (error) {
                    console.error('Failed to switch to new environment after onboarding:', error);
                }
            }
            window.dispatchEvent(new CustomEvent('configs-updated', {
                detail: {
                    source: 'environment-create',
                    name: payload && payload.name ? payload.name : (environment && environment.name ? environment.name : null),
                    environment: environment
                }
            }));
            this.onboardingForm.value = payload && payload.name ? payload.name : (environment && environment.name ? environment.name : '');
            await this.submitOnboardingStep();
            if (!this.activeOnboardingStep || this.activeOnboardingStep.input_type !== 'environment') {
                const formEl = this.$refs.onboardingEnvironmentForm;
                const component = formEl && formEl.__x ? formEl.__x.$data : null;
                if (component && typeof component.resetForm === 'function') {
                    component.resetForm();
                }
            }
        },
         async resetOnboarding() {
             if (!confirm('Are you sure you want to reset the onboarding process? This will clear all your settings.')) {
                 return;
             }
             this.overlaySaving = true;
             this.overlayError = '';
             try {
                 const response = await fetch('/api/webui/onboarding/reset', {
                     method: 'POST',
                     headers: {
                         'Content-Type': 'application/json'
                     }
                 });
                 if (!response.ok) {
                     throw new Error(`HTTP error! status: ${response.status}`);
                 }
                const data = await response.json();
                const defaultsPayload = data.resolved_defaults || data.defaults || {};
                const normalizedDefaults = this.normalizeDefaultsPayload(defaultsPayload);
                // Update state with fresh data
                this.defaults = normalizedDefaults;
                this.preserveDefaultsOption = normalizedDefaults.auto_preserve_defaults;
                this.onboardingSteps = data.onboarding.steps || [];
                this.overlayVisible = Boolean(data.onboarding.overlay_required);
                this.onboardingForm = { value: '' };
                this.onboardingAccelerate = { mode: 'auto', manualCount: 1, deviceIds: [] };
                await this.setActiveOnboardingStep();

                if (window.showToast) {
                    window.showToast('Onboarding reset successfully. Please enter your settings again.', 'success');
                }
                window.dispatchEvent(new CustomEvent('webui-defaults-updated', {
                    detail: {
                        defaults: data.defaults || {},
                        resolved_defaults: normalizedDefaults,
                        source: 'onboarding-reset'
                    }
                }));
            } catch (error) {
                console.error('Failed to reset onboarding', error);
                this.overlayError = 'Failed to reset onboarding. Please try again.';
            } finally {
                this.overlaySaving = false;
             }
         },
         async loadPreset(presetName) {
            try {
                const presets = {
                    'simple-local': [
                        {
                            id: "training-images",
                            type: "local",
                            dataset_type: "image",
                            instance_data_dir: "/path/to/images",
                            resolution: 1024,
                            resolution_type: "pixel_area",
                            caption_strategy: "textfile",
                            metadata_backend: "discovery",
                            cache_dir_vae: "cache/vae",
                            crop: true,
                            crop_style: "random",
                            crop_aspect: "square",
                            repeats: 0
                        },
                        {
                            id: "text-embeds",
                            type: "local",
                            dataset_type: "text_embeds",
                            default: true,
                            cache_dir: "cache/text",
                            write_batch_size: 128
                        }
                    ],
                    'aws-with-local-cache': [
                        {
                            id: "aws-images",
                            type: "aws",
                            dataset_type: "image",
                            aws_bucket_name: "my-training-bucket",
                            aws_data_prefix: "training-data/",
                            aws_endpoint_url: "",
                            aws_region_name: "us-east-1",
                            cache_dir_vae: "/local/cache/vae",
                            resolution: 1024,
                            resolution_type: "pixel_area",
                            caption_strategy: "textfile",
                            image_embeds: "local-image-embeds",
                            text_embeds: "local-text-embeds"
                        },
                        {
                            id: "local-image-embeds",
                            type: "local",
                            dataset_type: "image_embeds"
                        },
                        {
                            id: "local-text-embeds",
                            type: "local",
                            dataset_type: "text_embeds",
                            default: true,
                            cache_dir: "/local/cache/text"
                        }
                    ],
                    'video-training': [
                        {
                            id: "video-dataset",
                            type: "local",
                            dataset_type: "video",
                            instance_data_dir: "/path/to/videos",
                            resolution: 480,
                            resolution_type: "pixel_area",
                            caption_strategy: "textfile",
                            cache_dir_vae: "cache/vae/video",
                            crop: false,
                            video: {
                                num_frames: 125,
                                min_frames: 125,
                                is_i2v: true
                            }
                        },
                        {
                            id: "text-embeds",
                            type: "local",
                            dataset_type: "text_embeds",
                            default: true,
                            cache_dir: "cache/text/video"
                        }
                    ],
                    'controlnet-training': [
                        {
                            id: "training-images",
                            type: "local",
                            dataset_type: "image",
                            instance_data_dir: "/path/to/images",
                            resolution: 1024,
                            resolution_type: "pixel_area",
                            caption_strategy: "textfile",
                            cache_dir_vae: "cache/vae",
                            conditioning_data: "canny-conditioning",
                            conditioning: [
                                {
                                    type: "canny",
                                    params: {
                                        low_threshold: 100,
                                        high_threshold: 200
                                    }
                                }
                            ]
                        },
                        {
                            id: "canny-conditioning",
                            type: "local",
                            dataset_type: "conditioning",
                            conditioning_type: "controlnet"
                        },
                        {
                            id: "text-embeds",
                            type: "local",
                            dataset_type: "text_embeds",
                            default: true,
                            cache_dir: "cache/text"
                        }
                    ]
                };

                if (presets[presetName]) {
                    // Check if current datasets exist before loading preset
                    if (this.datasets && this.datasets.length > 0) {
                        if (!confirm('Are you sure? Your current dataloader config will be unlinked.')) {
                            return; // User cancelled, don't load preset
                        }
                    }

                    this.datasets = this.normalizeDatasets(JSON.parse(JSON.stringify(presets[presetName])));
                    this.refreshDatasetsJson();
                    this.hasUnsavedChanges = true;
                    window.showToast(`Loaded ${presetName} preset (unsaved)`, 'info');
                }
            } catch (error) {
                console.error('Error loading preset:', error);
                window.showToast('Failed to load preset', 'error');
            }
        },
        confirmSaveDatasets() {
            // Hide dialog and proceed with save
            this.showSaveDialog = false;
            const resolve = this.saveDialogResolve;
            this.saveDialogResolve = null;

            // Call save with the selected options
            this.saveDatasets({
                showToast: true,
                createBackup: this.createBackupOption,
                skipConfirmation: true
            }).then(() => {
                if (resolve) resolve();
            });
        },
        cancelSaveDatasets() {
            // Hide dialog and resolve the promise
            this.showSaveDialog = false;
            const resolve = this.saveDialogResolve;
            this.saveDialogResolve = null;
            if (resolve) resolve();
        },
        hasFieldError(datasetId, fieldName) {
            // Check if a specific field has a validation error
            const fieldKey = `${datasetId}.${fieldName}`;
            return this.datasetValidationErrors.hasOwnProperty(fieldKey);
        },
        getFieldError(datasetId, fieldName) {
            // Get the error message for a specific field
            const fieldKey = `${datasetId}.${fieldName}`;
            return this.datasetValidationErrors[fieldKey] || '';
        },
        getDatasetTypeIcon(type) {
            // Return emoji icon for dataset type
            const icons = {
                'image': '🖼️',
                'video': '🎬',
                'conditioning': '🎨',
                'text_embeds': '💾',
                'image_embeds': '💾'
            };
            return icons[type] || '📁';
        },

        // Form change tracking methods
        markFormDirty() {
            this.formDirty = true;
            this._skipNextClean = true;
        },

        markFormClean() {
            this.formDirty = false;
            this._skipNextClean = false;
            // Capture current values for cross-tab persistence
            this.captureFormValues();
            // Store current form data as original
            const formElement = document.getElementById('trainer-form');
            if (formElement) {
                const formData = new FormData(formElement);
                this.originalFormData = {};
                for (const [key, value] of formData.entries()) {
                    this.originalFormData[key] = value;
                }
            }
        },

        // Check if form has changed
        checkFormDirty() {
            const formElement = document.getElementById('trainer-form');
            if (!formElement) return;

            const formData = new FormData(formElement);
            let isDirty = false;

            // Check for changes
            for (const [key, value] of formData.entries()) {
                if (this.originalFormData[key] !== value) {
                    isDirty = true;
                    break;
                }
            }

            // Check for removed fields
            for (const key in this.originalFormData) {
                if (!formData.has(key)) {
                    isDirty = true;
                    break;
                }
            }

            this.formDirty = isDirty;
        },

        cssEscape(name) {
            if (window.CSS && window.CSS.escape) {
                return window.CSS.escape(name);
            }
            return name.replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`);
        },

        captureFormValues() {
            const form = document.getElementById('trainer-form');
            if (!form) return;

            // Start with existing values to preserve cross-tab data
            const values = { ...this.formValueStore };

            const elements = form.querySelectorAll('input[name], select[name], textarea[name]');
            const processed = new Set();
            elements.forEach((el) => {
                const name = el.name;
                if (!name || processed.has(name)) {
                    return;
                }
                processed.add(name);

                // Canonicalize key to ensure consistency with config (dashed)
                const key = this.canonicalizeKey(name) || name;

                // Update only the fields present in current form
                values[key] = this.getFieldDescriptor(form, name);
            });
            this.formValueStore = values;
        },

        getFieldDescriptor(form, name) {
            const selector = `[name="${this.cssEscape(name)}"]`;
            const nodes = form.querySelectorAll(selector);
            if (!nodes.length) {
                return { kind: 'single', value: '' };
            }

            const checkboxNodes = Array.from(nodes).filter((node) => (node.type || '').toLowerCase() === 'checkbox');
            if (checkboxNodes.length) {
                const checked = checkboxNodes
                    .filter((node) => node.checked)
                    .map((node) => node.value || 'on');
                return { kind: 'checkbox', value: checked, multi: checkboxNodes.length > 1 };
            }

            const first = nodes[0];
            const tag = first.tagName;
            const type = (first.type || '').toLowerCase();

            if (type === 'radio') {
                const checked = Array.from(nodes).find((node) => node.checked);
                return { kind: 'radio', value: checked ? checked.value : null };
            }

            if (tag === 'SELECT' && first.multiple) {
                const selected = Array.from(first.selectedOptions).map((opt) => opt.value);
                return { kind: 'multiple', value: selected };
            }

            return { kind: 'single', value: first.value };
        },

        updateStoredValue(field) {
            if (!field || !field.name) {
                return;
            }
            const form = document.getElementById('trainer-form');
            if (!form) return;

            // Canonicalize key for storage consistency
            const key = this.canonicalizeKey(field.name) || field.name;

            this.formValueStore = {
                ...this.formValueStore,
                [key]: this.getFieldDescriptor(form, field.name),
            };
        },

        canonicalizeKey(name) {
            if (!name || typeof name !== 'string' || name.startsWith('__')) {
                return null;
            }
            if (name.startsWith('--')) {
                return name;
            }
            const trimmed = name.replace(/^[-]+/, '');
            return trimmed ? `--${trimmed}` : name;
        },

        applyStoredValues() {
            const form = document.getElementById('trainer-form');
            if (!form || !this.formValueStore) {
                console.log('No form or formValueStore to apply');
                return;
            }

            console.log('Applying stored values:', Object.keys(this.formValueStore).length, 'fields');

            Object.entries(this.formValueStore).forEach(([name, entry]) => {
                if (!entry) return;

                let selector = `[name="${this.cssEscape(name)}"]`;
                let nodes = form.querySelectorAll(selector);

                // Robustness fix: If direct match not found, try alternate naming (dashed vs non-dashed)
                // This handles the "mish-mash" of parameter naming conventions
                if (!nodes.length) {
                    let altName = null;
                    if (name.startsWith('--')) {
                        // Try stripping dashes: --report_to -> report_to
                        altName = name.substring(2);
                    } else {
                        // Try adding dashes: report_to -> --report_to
                        altName = `--${name}`;
                    }

                    if (altName) {
                        const altSelector = `[name="${this.cssEscape(altName)}"]`;
                        const altNodes = form.querySelectorAll(altSelector);
                        if (altNodes.length > 0) {
                            nodes = altNodes;
                            // console.debug(`[TRAINER] Matched stored key "${name}" to DOM element "${altName}"`);
                        }
                    }
                }

                if (!nodes.length) {
                    return;
                }

                if (entry.kind === 'checkbox') {
                    const values = Array.isArray(entry.value) ? entry.value : [];
                    nodes.forEach((node) => {
                        const val = node.value || 'on';
                        node.checked = values.includes(val);
                    });
                } else if (entry.kind === 'radio') {
                    nodes.forEach((node) => {
                        node.checked = entry.value !== null && node.value === entry.value;
                    });
                } else if (entry.kind === 'multiple') {
                    nodes.forEach((node) => {
                        if (node.tagName === 'SELECT') {
                            const set = new Set(Array.isArray(entry.value) ? entry.value : []);
                            Array.from(node.options).forEach((opt) => {
                                opt.selected = set.has(opt.value);
                            });
                        }
                    });
                } else {
                    nodes.forEach((node) => {
                        const oldValue = node.value;
                        node.value = entry.value ?? '';
                        if (oldValue !== (entry.value ?? '')) {
                            // console.log(`Restored ${name}: "${oldValue}" -> "${entry.value ?? ''}"`);
                        }
                    });
                }
            });
        },

        handleFieldInput(event) {
            if (event && event.target && event.target.name) {
                this.updateStoredValue(event.target);
            }
            this.checkFormDirty();
        },

        applyEventPreferences() {
            const eventList = document.getElementById('event-list');
            if (!eventList) {
                return;
            }

            const defaults = this.defaults || {};
            let interval = parseInt(defaults.event_polling_interval || 5, 10);
            if (!Number.isFinite(interval) || interval < 1) {
                interval = 5;
            }

            const enabled = defaults.event_stream_enabled !== false;
            const triggers = ['load'];
            if (enabled) {
                triggers.push(`every ${interval}s`);
            }
            triggers.push('refresh');

            eventList.setAttribute('hx-trigger', triggers.join(', '));
            if (window.htmx && typeof window.htmx.process === 'function') {
                window.htmx.process(eventList);
            }
        },

        ensureCompleteFormData(formData) {
            if (!this.formValueStore) return;
            Object.entries(this.formValueStore).forEach(([name, entry]) => {
                if (!entry) return;
                const hasExisting = formData.has(name);

                if (entry.kind === 'checkbox') {
                    const values = Array.isArray(entry.value) ? entry.value : [];
                    formData.delete(name);
                    if (entry.multi) {
                        if (values.length === 0) {
                            formData.append(name, 'false');
                        } else {
                            values.forEach((val) => {
                                formData.append(name, val != null ? String(val) : 'true');
                            });
                        }
                    } else {
                        formData.append(name, values.length > 0 ? String(values[0]) : 'false');
                    }
                    return;
                }

                if (entry.kind === 'multiple') {
                    if (hasExisting) {
                        return;
                    }
                    const values = Array.isArray(entry.value) ? entry.value : [];
                    values.forEach((val) => {
                        formData.append(name, val != null ? String(val) : '');
                    });
                    return;
                }

                if (entry.kind === 'radio') {
                    if (!hasExisting && entry.value !== null && entry.value !== undefined) {
                        formData.append(name, String(entry.value));
                    }
                    return;
                }

                if (!hasExisting) {
                    formData.append(name, entry.value != null ? String(entry.value) : '');
                }
            });
        },

        normalizeCheckboxFormData(formData) {
            const formElement = document.getElementById('trainer-form');
            if (!formElement) {
                return;
            }

            const groups = new Map();
            formElement.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
                if (!checkbox.name) {
                    return;
                }
                if (!groups.has(checkbox.name)) {
                    groups.set(checkbox.name, []);
                }
                groups.get(checkbox.name).push(checkbox);
            });

            groups.forEach((checkboxes, name) => {
                formData.delete(name);
                const checked = checkboxes.filter((node) => node.checked);

                if (checkboxes.length > 1) {
                    if (checked.length === 0) {
                        formData.append(name, 'false');
                    } else {
                        checked.forEach((node) => {
                            formData.append(name, node.value || 'true');
                        });
                    }
                    return;
                }

                const single = checkboxes[0];
                formData.append(name, single.checked ? (single.value || 'true') : 'false');
            });
        },

        appendConfigValuesToFormData(formData, config) {
            if (!config || typeof config !== 'object') {
                return;
            }

            console.log('[TRAINER] appendConfigValuesToFormData called with config keys:', Object.keys(config));

            const metadataMap = window.dependencyManager?.fieldMetadata;
            const hasUserOverride = (name) => {
                if (!name) {
                    return false;
                }
                const variants = new Set();
                variants.add(name);
                const trimmed = name.startsWith('--') ? name.slice(2) : name;
                if (trimmed) {
                    variants.add(trimmed);
                    variants.add(`--${trimmed}`);
                    const snake = trimmed
                        .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
                        .replace(/[-\s]+/g, '_');
                    if (snake) {
                        variants.add(snake);
                        variants.add(`--${snake}`);
                        const lowerSnake = snake.toLowerCase();
                        variants.add(lowerSnake);
                        variants.add(`--${lowerSnake}`);
                    }
                    const lowerTrimmed = trimmed.toLowerCase();
                    variants.add(lowerTrimmed);
                    variants.add(`--${lowerTrimmed}`);
                }

                for (const variant of variants) {
                    if (variant && formData.has(variant)) {
                        return true;
                    }
                }
                return false;
            };
            const appendValue = (name, descriptor, options = {}) => {
                const { force = false } = options;
                if (!descriptor) {
                    return;
                }

                if (!force && hasUserOverride(name)) {
                    console.log(`[TRAINER] Skipping ${name} – user supplied value present`);
                    return;
                }

                // Delete existing values so we can replace them
                const hadValue = formData.has(name);
                if (hadValue) {
                    const oldValue = formData.get(name);
                    formData.delete(name);
                    console.log(`[TRAINER] Replacing ${name}: "${oldValue}" -> "${descriptor.value}"`);
                } else {
                    console.log(`[TRAINER] Adding new ${name}: "${descriptor.value}"`);
                }

                switch (descriptor.kind) {
                    case 'checkbox': {
                        if (descriptor.multi) {
                            (descriptor.value || []).forEach((val) => formData.append(name, val));
                            break;
                        }

                        let checkboxValue;
                        if (Array.isArray(descriptor.value)) {
                            checkboxValue = descriptor.value.length > 0 ? descriptor.value[0] : 'false';
                        } else if (typeof descriptor.value === 'boolean') {
                            checkboxValue = descriptor.value ? 'true' : 'false';
                        } else if (descriptor.value === null || descriptor.value === undefined) {
                            checkboxValue = 'false';
                        } else {
                            const normalized = String(descriptor.value).trim().toLowerCase();
                            if (['true', '1', 'yes', 'on'].includes(normalized)) {
                                checkboxValue = 'true';
                            } else if (['false', '0', 'no', 'off'].includes(normalized)) {
                                checkboxValue = 'false';
                            } else {
                                checkboxValue = normalized || 'false';
                            }
                        }

                        formData.append(name, checkboxValue);
                        break;
                    }
                    case 'multiple':
                        (descriptor.value || []).forEach((val) => formData.append(name, val));
                        break;
                    case 'radio':
                        if (descriptor.value !== null && descriptor.value !== undefined) {
                            formData.append(name, String(descriptor.value));
                        }
                        break;
                    default:
                        if (descriptor.value !== undefined && descriptor.value !== null) {
                            formData.append(name, String(descriptor.value));
                        }
                        break;
                }
            };

            const buildDescriptor = (name, value) => {
                if (value === undefined || value === null) {
                    return null;
                }

                const metadata = metadataMap?.get?.(name);
                const fieldType = metadata?.field_type || metadata?.fieldType;

                if (Array.isArray(value)) {
                    const primitives = value.every((item) => item === null || ['string', 'number', 'boolean'].includes(typeof item));
                    if (!primitives) {
                        return null;
                    }
                    return {
                        kind: 'multiple',
                        value: value.map((item) => (item === null || item === undefined ? '' : String(item))),
                    };
                }

                if (typeof value === 'boolean') {
                    if (fieldType === 'checkbox') {
                        return {
                            kind: 'checkbox',
                            value: value ? ['true'] : [],
                            multi: false,
                        };
                    }
                    return { kind: 'single', value: value ? 'true' : 'false' };
                }

                if (typeof value === 'number') {
                    return { kind: 'single', value: String(value) };
                }

                if (typeof value === 'string') {
                    return { kind: 'single', value };
                }

                if (typeof value === 'object') {
                    try {
                        return { kind: 'single', value: JSON.stringify(value) };
                    } catch (error) {
                        console.warn('[TRAINER] Failed to serialize object value for', name, error);
                        return null;
                    }
                }

                return null;
            };

            Object.entries(config).forEach(([key, value]) => {
                if (!key || key.startsWith('_')) {
                    return;
                }

                if (!key.startsWith('--')) {
                    return;
                }

                const descriptor = buildDescriptor(key, value);
                appendValue(key, descriptor);
            });

            if (!formData.has('--job_id')) {
                let jobId =
                    config['--job_id'] ??
                    config['job_id'] ??
                    this.formValueStore?.['--job_id']?.value ??
                    this.formValueStore?.['job_id']?.value;

                if (Array.isArray(jobId)) {
                    jobId = jobId.length ? jobId[0] : '';
                }

                if (jobId === undefined || jobId === null) {
                    const jobInput = document.getElementById('job_id');
                    const dashedInput = document.querySelector('[name="--job_id"]');
                    jobId = jobInput?.value ?? dashedInput?.value ?? '';
                }

                formData.append('--job_id', String(jobId));
            }
        },

        seedFormValueStoreFromConfig(config) {
            if (!config || typeof config !== 'object') {
                return;
            }

            const metadataMap = window.dependencyManager?.fieldMetadata;
            const buildDescriptor = (name, value) => {
                if (value === undefined || value === null) {
                    return { kind: 'single', value: '' };
                }

                const metadata = metadataMap?.get?.(name);
                const fieldType = metadata?.field_type || metadata?.fieldType;

                if (Array.isArray(value)) {
                    const primitives = value.every((item) => item === null || ['string', 'number', 'boolean'].includes(typeof item));
                    if (!primitives) {
                        return null;
                    }
                    return {
                        kind: 'multiple',
                        value: value.map((item) => (item === null || item === undefined ? '' : String(item))),
                    };
                }

                if (typeof value === 'boolean') {
                    const descriptor = {
                        kind: 'single',
                        value: value ? 'true' : 'false',
                    };

                    if (fieldType === 'checkbox') {
                        descriptor.kind = 'checkbox';
                        descriptor.value = value ? ['true'] : [];
                        descriptor.multi = false;
                    }

                    return descriptor;
                }

                if (typeof value === 'number') {
                    return { kind: 'single', value: String(value) };
                }

                if (typeof value === 'string') {
                    return { kind: 'single', value };
                }

                if (typeof value === 'object') {
                    try {
                        return { kind: 'single', value: JSON.stringify(value, null, 2) };
                    } catch (error) {
                        console.warn('[TRAINER] Failed to serialize object value for seed store', name, error);
                        return { kind: 'single', value: '' };
                    }
                }

                return { kind: 'single', value: '' };
            };

            const baseStore = {};
            Object.entries(config).forEach(([key, value]) => {
                if (!key || key.startsWith('_')) {
                    return;
                }

                const descriptor = buildDescriptor(key, value);
                if (descriptor) {
                    baseStore[key] = descriptor;
                }
            });

            this.formValueStore = {
                ...baseStore,
                ...this.formValueStore,
            };
        },

        // Save config with options dialog
        async saveConfig() {
            const autoPreserve = this.autoPreserveEnabled(this.defaults ? this.defaults.auto_preserve_defaults : undefined);

            // Show the save dialog
            this.showConfigSaveDialog = true;
            this.createConfigBackupOption = false;
            this.preserveDefaultsOption = autoPreserve;

            // Return a promise that will be resolved by the dialog buttons
            return new Promise((resolve) => {
                this.configSaveDialogResolve = resolve;
            });
        },

        // Handle save dialog confirmation
        confirmSaveConfig() {
            this.showConfigSaveDialog = false;
            const resolve = this.configSaveDialogResolve;
            this.configSaveDialogResolve = null;

            // Call the actual save
            this.doSaveConfig({
                createBackup: this.createConfigBackupOption,
                preserveDefaults: this.preserveDefaultsOption
            }).then(() => {
                if (resolve) resolve();
            });
        },

        // Cancel save dialog
        cancelSaveConfig() {
            this.showConfigSaveDialog = false;
            const resolve = this.configSaveDialogResolve;
            this.configSaveDialogResolve = null;
            if (resolve) resolve();
        },

// Perform the actual save
        async doSaveConfig(options = {}) {
            const formElement = document.getElementById('trainer-form');
            if (!formElement) return;

            try {
                let formData;

                if (options.replace) {
                    // Replace mode: Create fresh FormData solely from activeEnvironmentConfig
                    // This bypasses the DOM form elements, ensuring stale UI values don't persist
                    formData = new FormData();

                    // Signal server to replace the config instead of merging
                    formData.append('__replace_active_config__', 'true');

                    if (options.preserveDefaults) {
                        formData.append('preserve_defaults', 'true');
                    }
                    if (options.createBackup) {
                        formData.append('create_backup', 'true');
                    }

                    console.log('[TRAINER] Saving config in replace mode (ignoring form DOM)');
                    this.appendConfigValuesToFormData(formData, this.activeEnvironmentConfig);

                    // Ensure job_id is present if possible
                    if (!formData.has('--job_id')) {
                        const jobId = this.formValueStore?.['--job_id']?.value ||
                                      this.formValueStore?.['job_id']?.value;
                        if (jobId) {
                            formData.append('--job_id', String(jobId));
                        }
                    }
                } else {
                    // Normal mode: Merge form DOM with config
                    formData = new FormData(formElement);

                    // Normalize checkboxes to single true/false entries
                    this.normalizeCheckboxFormData(formData);

                    // Ensure values from other tabs are included
                    this.ensureCompleteFormData(formData);

                    // Remove forced fields from FormData so they are repopulated from the active config
                    // This is used by the wizard to ensure its changes take precedence over stale DOM values
                    if (options.forceFields && Array.isArray(options.forceFields)) {
                        const variants = new Set();
                        options.forceFields.forEach(field => {
                            if (!field) return;
                            variants.add(field);
                            if (!field.startsWith('--')) {
                                variants.add(`--${field}`);
                            } else {
                                variants.add(field.substring(2));
                            }
                        });

                        // Remove all variants from formData
                        variants.forEach(variant => {
                            if (formData.has(variant)) {
                                console.log(`[TRAINER] forcing update for ${variant} (removing from FormData)`);
                                formData.delete(variant);
                            }
                        });
                    }

                    // Add save options
                    if (options.preserveDefaults) {
                        formData.append('preserve_defaults', 'true');
                    }
                    if (options.createBackup) {
                        formData.append('create_backup', 'true');
                    }

                    // Ensure values from the active config are present
                    this.appendConfigValuesToFormData(formData, this.activeEnvironmentConfig);

                    // Final checkbox normalization after all merges
                    this.normalizeCheckboxFormData(formData);
                }

                const params = {};
                formData.forEach((value, key) => {
                    if (Object.prototype.hasOwnProperty.call(params, key)) {
                        const existing = params[key];
                        if (Array.isArray(existing)) {
                            existing.push(value);
                        } else {
                            params[key] = [existing, value];
                        }
                    } else {
                        params[key] = value;
                    }
                });

                const saveButton = document.getElementById('confirmSaveConfigButton');
                const indicatorTargets = [];
                const indicatorTargetSelector = saveButton ? saveButton.getAttribute('hx-indicator') : null;
                const indicatorTarget = indicatorTargetSelector ? document.querySelector(indicatorTargetSelector) : null;
                if (indicatorTarget) {
                    indicatorTargets.push({ element: indicatorTarget, forceDisplay: true });
                }

                const primaryIndicatorButton = document.querySelector('button[hx-indicator], a[hx-indicator]');
                const primaryIndicatorSelector = primaryIndicatorButton ? primaryIndicatorButton.getAttribute('hx-indicator') : null;
                const primaryIndicator = primaryIndicatorSelector ? document.querySelector(primaryIndicatorSelector) : null;
                if (primaryIndicator && (!indicatorTarget || primaryIndicator !== indicatorTarget)) {
                    indicatorTargets.push({ element: primaryIndicator, forceDisplay: false });
                }

                const loadingIndicator = document.querySelector('[data-htmx-indicator]');

                const dispatchHTMXEvent = (name, detail) => {
                    try {
                        document.body.dispatchEvent(new CustomEvent(name, { detail }));
                    } catch (err) {
                        console.debug(`Failed to dispatch ${name}`, err);
                    }
                };

                const requestDetail = {
                    path: '/api/training/config',
                    verb: 'post',
                    method: 'post',
                    parameters: params,
                    headers: { 'HX-Request': 'true' },
                    xhr: null,
                };

                const activateIndicators = () => {
                    indicatorTargets.forEach(({ element, forceDisplay }) => {
                        if (!element) {
                            return;
                        }
                        element.classList.add('htmx-request');
                        if (forceDisplay) {
                            element.classList.remove('d-none');
                            if (element.style && !element.style.display) {
                                element.style.display = 'inline-flex';
                            }
                        }
                    });
                };
                const deactivateIndicators = () => {
                    indicatorTargets.forEach(({ element, forceDisplay }) => {
                        if (!element) {
                            return;
                        }
                        element.classList.remove('htmx-request');
                        if (forceDisplay) {
                            element.classList.add('d-none');
                            if (element.style && element.style.display === 'inline-flex') {
                                element.style.display = '';
                            }
                        }
                    });
                };

                activateIndicators();
                if (loadingIndicator) {
                    loadingIndicator.classList.remove('d-none');
                    loadingIndicator.classList.add('htmx-request');
                }
                dispatchHTMXEvent('htmx:configRequest', requestDetail);

                let response;
                let responseText = '';
                let successful = false;
                const buildResponseShim = (res, text) => {
                    const safeText = text || '';
                    if (!res) {
                        return {
                            status: 0,
                            responseText: safeText,
                            getResponseHeader: () => null,
                            headers: null,
                        };
                    }
                    return {
                        status: res.status,
                        responseText: safeText,
                        getResponseHeader: (header) => {
                            if (!header) {
                                return null;
                            }
                            try {
                                return res.headers.get(header);
                            } catch (err) {
                                console.debug('Failed to read response header', err);
                                return null;
                            }
                        },
                        headers: res.headers,
                    };
                };
                try {
                    response = await fetch('/api/training/config', {
                        method: 'POST',
                        body: formData
                    });
                    responseText = await response.text();
                    successful = response.ok;

                    if (response.ok) {
                        this.markFormClean();
                        window.showToast('Configuration saved', 'success');

                        const feedbackTarget = document.getElementById('htmx-save-feedback');
                        if (feedbackTarget) {
                            feedbackTarget.innerHTML = responseText || '<div class="text-success">Configuration saved</div>';
                            feedbackTarget.classList.remove('visually-hidden');
                            if (!feedbackTarget.dataset.persist) {
                                setTimeout(() => {
                                    feedbackTarget.classList.add('visually-hidden');
                                }, 3500);
                            }
                        }

                        // Dispatch event for other components (e.g., publishing tab)
                        window.dispatchEvent(new CustomEvent('config-saved'));

                        try {
                            await this.fetchActiveEnvironmentConfig();
                            if (this.showConfigJsonModal) {
                                this.resetConfigJsonDraft();
                            }
                        } catch (refreshError) {
                            console.error('Failed to refresh configuration after save:', refreshError);
                        }
                    } else {
                        let detail = null;
                        try {
                            detail = JSON.parse(responseText);
                        } catch (_) {
                            detail = null;
                        }
                        const message =
                            (detail && (detail.detail || detail.message)) ||
                            (responseText || '').trim() ||
                            'Failed to save configuration';
                        window.showToast(message, 'error');
                    }
                } catch (error) {
                    successful = false;
                    responseText = error instanceof Error ? error.message : String(error);
                    console.error('Error saving config:', error);
                    window.showToast('Failed to save configuration', 'error');
                    throw error;
                } finally {
                    deactivateIndicators();
                    if (loadingIndicator) {
                        loadingIndicator.classList.add('d-none');
                        loadingIndicator.classList.remove('htmx-request');
                    }
                    dispatchHTMXEvent('htmx:afterRequest', {
                        ...requestDetail,
                        successful,
                        xhr: buildResponseShim(response, responseText),
                        elt: saveButton
                    });
                }
            } catch (error) {
                console.error('Error saving config:', error);
                window.showToast('Failed to save configuration', 'error');
            }
        }
    }
}
</script>

<div class="dashboard-wrapper"
     x-data="trainerComponent()"
     x-init="(() => {
         // Store reference to Alpine component for external access
         Alpine.store('trainer', $data);

         // Load initial tab content
         $nextTick(() => {
             const updateHash = !!window.location.hash;
             $data.activateTab($data.activeTab, updateHash);
         });

         // Listen for browser back/forward navigation
         window.addEventListener('hashchange', () => {
             const tab = window.location.hash.slice(1) || 'basic';
             if (tab !== $data.activeTab) {
                 $data.activateTab(tab, false);
             }
         });

        const handleTrainingProgress = (event) => {
            const detail = event.detail || {};

            if (detail.reset) {
                $data.trainingProgress = {};
                $data.isTraining = false;
                $data.showTrainingProgress = false;
                if (window.eventHandler && typeof window.eventHandler.resetTrainingState === 'function') {
                    window.eventHandler.resetTrainingState();
                }
                return;
            }

            const loss = detail.loss;
            const formattedLoss = (typeof loss === 'number' && !isNaN(loss))
                ? loss.toFixed(4)
                : (loss ?? 'N/A');

            const lr = detail.learning_rate ?? detail.lr;
            const formattedLr = (typeof lr === 'number' && !isNaN(lr))
                ? lr.toExponential(2)
                : (lr ?? 'N/A');

            const rawPercent = Number(detail.percent ?? detail.percentage ?? 0);
            const safePercent = Number.isFinite(rawPercent) ? rawPercent : 0;
            const normalizedPercent = Math.max(0, Math.min(100, safePercent));
            const roundedPercent = Math.round(normalizedPercent * 100) / 100;

            $data.trainingProgress = {
                percent: roundedPercent,
                step: detail.step || detail.current_step || 0,
                total_steps: detail.total_steps || detail.total || 0,
                epoch: detail.epoch || 0,
                loss: formattedLoss,
                learning_rate: formattedLr,
            };
            $data.isTraining = true;
            $data.showTrainingProgress = true;
        };

        const handleTrainingStatus = (event) => {
            const detail = event.detail || {};
            const status = String(detail.status || '').toLowerCase();
            if (['running', 'starting', 'initializing'].includes(status)) {
                $data.isTraining = true;
                if (detail.progress) {
                    handleTrainingProgress({ detail: detail.progress });
                }
            } else if (status) {
                $data.isTraining = false;
                if (status === 'completed' && $data.trainingProgress && ($data.trainingProgress.percent || 0) < 100) {
                    $data.trainingProgress = {
                        ...$data.trainingProgress,
                        percent: 100,
                    };
                } else if (['failed', 'error', 'cancelled', 'stopped'].includes(status)) {
                    handleTrainingProgress({ detail: { reset: true } });
                }
            }
        };

        window.addEventListener('training-progress', handleTrainingProgress);
        window.addEventListener('training-status', handleTrainingStatus);

        window.addEventListener('beforeunload', () => {
            window.removeEventListener('training-progress', handleTrainingProgress);
            window.removeEventListener('training-status', handleTrainingStatus);
        });

         // Initialize other components
         $nextTick(async () => {
             await $data.initializeWebUIState();
             await $data.loadEnvironmentConfigs();

             // Ensure active environment config is loaded and form is populated
             if ($data.activeEnvironment) {
                 // Suppress form dirty marking during initial load
                 $data._skipNextClean = true;
                 await $data.fetchActiveEnvironmentConfig();
                 // Wait for DOM to be ready before applying values
                 await $nextTick();
                 $data.applyStoredValues();
                 // Mark form as clean after all initialization
                 $data.markFormClean();
                 $data._skipNextClean = false;
             }

             // Set up form change tracking
             const form = document.getElementById('trainer-form');
            if (form) {
                // Add change listeners to all form inputs
                const handleFormChange = (event) => {
                    $data.handleFieldInput(event);
                };

                form.addEventListener('input', handleFormChange);
                form.addEventListener('change', handleFormChange);

                // Listen for HTMX after swap events to reapply listeners
                document.body.addEventListener('htmx:afterSwap', (evt) => {
                    if (evt.detail.target.id !== 'tab-content') {
                        return;
                    }

                    // Use a longer delay and requestAnimationFrame to ensure DOM is fully updated
                    setTimeout(() => {
                        requestAnimationFrame(() => {
                            if ($data._preservingFormState) {
                                console.log('Applying stored form values after tab switch');
                                $data.applyStoredValues();
                                // Mark clean after tab switch - we just loaded fresh content from server
                                // Don't call checkFormDirty() because originalFormData is from a different tab
                                $data.markFormClean();
                            } else if (!$data._skipNextClean) {
                                $data.markFormClean();
                            }

                            delete $data._preservingFormState;
                            $data._skipNextClean = false;
                        });
                    }, 150);
                });

                // Also listen for afterSettle event which fires after all animations complete
                document.body.addEventListener('htmx:afterSettle', (evt) => {
                    if (evt.detail.target.id !== 'tab-content') {
                        return;
                    }

                    // Double-check if we still need to preserve form state
                    if ($data._preservingFormState) {
                        console.log('Applying stored values on afterSettle');
                        $data.applyStoredValues();
                        delete $data._preservingFormState;
                    }
                });
             }

            await $data.loadDatasetsAfterEnvironmentChange();

             $data.ensureEventDockWithinBounds();
         });

         window.addEventListener('resize', () => {
             $data.ensureEventDockWithinBounds();
         });
     })()">

    <div id="save-config-indicator"
         class="htmx-indicator d-none"
         aria-hidden="true">
        <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
        <span class="visually-hidden">Saving configuration…</span>
    </div>

    <div id="save-config-loading"
         data-htmx-indicator
         class="save-config-loading-banner d-none"
         role="status"
         aria-live="polite">
        <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
        <span>Saving configuration…</span>
    </div>

    <div id="htmx-save-feedback" class="visually-hidden" aria-live="polite"></div>

    <style>
    .dashboard-wrapper {
        position: relative;
    }
    .save-config-loading-banner {
        display: none;
        position: absolute;
        top: 1rem;
        right: 1rem;
        z-index: 1055;
        padding: 0.5rem 0.75rem;
        border-radius: 0.5rem;
        background: rgba(15, 23, 42, 0.92);
        color: #f8fafc;
        box-shadow: 0 10px 30px rgba(15, 23, 42, 0.35);
        align-items: center;
        gap: 0.5rem;
    }
    .save-config-loading-banner:not(.d-none) {
        display: flex;
    }
    </style>

    <div class="onboarding-overlay"
         x-show="overlayVisible"
         x-transition.opacity.duration.150ms>
        <div class="onboarding-modal">
            <h2 x-text="activeOnboardingStep ? activeOnboardingStep.title : 'Welcome to SimpleTuner'"></h2>
            <p class="onboarding-prompt" x-text="activeOnboardingStep ? activeOnboardingStep.prompt : ''"></p>
            <form @submit.prevent="submitOnboardingStep">
                <div class="mb-3" x-show="activeOnboardingStep && activeOnboardingStep.input_type === 'directory'">
                    <label class="form-label" x-text="activeOnboardingStep ? activeOnboardingStep.title : ''"></label>
                    <input type="text"
                           id="onboarding-input"
                           name="onboarding_input"
                           class="form-control"
                           x-model="onboardingForm.value"
                           :placeholder="(activeOnboardingStep && activeOnboardingStep.applies_to_default && defaults[activeOnboardingStep.applies_to_default]) || '/path/to/output'"
                           x-ref="onboardingInput">
                    <div class="form-text">
                        Example: /home/you/simpletuner-output
                    </div>
                </div>
                <template x-if="activeOnboardingStep && activeOnboardingStep.input_type === 'accelerate_auto'">
                    <div class="mb-3">
                        <label class="form-label">GPU launch strategy</label>
                        <div class="form-check">
                            <input class="form-check-input"
                                   type="radio"
                                   id="accelerate-mode-disabled"
                                   value="disabled"
                                   x-model="onboardingAccelerate.mode"
                                   name="accelerate_mode">
                            <label class="form-check-label" for="accelerate-mode-disabled">
                                Single GPU only
                            </label>
                        </div>
                        <div class="form-check">
                            <input class="form-check-input"
                                   type="radio"
                                   id="accelerate-mode-auto"
                                   value="auto"
                                   x-model="onboardingAccelerate.mode"
                                   name="accelerate_mode">
                            <label class="form-check-label" for="accelerate-mode-auto">
                                Auto-detect available GPUs
                                <span class="text-muted" x-show="gpuInventory.devices.length">&mdash; will launch <span x-text="gpuInventory.optimalProcesses"></span> processes</span>
                            </label>
                        </div>
                        <div class="form-check">
                            <input class="form-check-input"
                                   type="radio"
                                   id="accelerate-mode-manual"
                                   value="manual"
                                   x-model="onboardingAccelerate.mode"
                                   name="accelerate_mode">
                            <label class="form-check-label" for="accelerate-mode-manual">
                                Choose GPUs manually
                            </label>
                        </div>
                        <div class="form-check">
                            <input class="form-check-input"
                                   type="radio"
                                   id="accelerate-mode-hardware"
                                   value="hardware"
                                   x-model="onboardingAccelerate.mode"
                                   name="accelerate_mode">
                            <label class="form-check-label" for="accelerate-mode-hardware">
                                Use num_processes from Hardware tab
                            </label>
                        </div>

                        <template x-if="gpuInventory.loading">
                            <div class="alert alert-info mt-3 py-2">
                                Detecting GPUs...
                            </div>
                        </template>
                        <template x-if="!gpuInventory.loading && gpuInventory.error">
                            <div class="alert alert-warning mt-3 py-2" x-text="gpuInventory.error"></div>
                        </template>

                        <template x-if="!gpuInventory.loading && gpuInventory.devices.length">
                            <div class="gpu-device-list mt-3">
                                <div class="fw-semibold mb-2">Detected GPUs</div>
                                <div class="list-group">
                                    <template x-for="device in gpuInventory.devices" :key="device.index">
                                        <label class="list-group-item d-flex align-items-center justify-content-between">
                                            <div>
                                                <span class="fw-medium" x-text="device.name"></span>
                                                <span class="text-muted ms-2" x-show="device.memory_gb" x-text="device.memory_gb + ' GB'"></span>
                                            </div>
                                            <input type="checkbox"
                                                   class="form-check-input"
                                                   x-model.number="onboardingAccelerate.deviceIds"
                                                   :value="device.index"
                                                   :disabled="onboardingAccelerate.mode !== 'manual'">
                                        </label>
                                    </template>
                                </div>
                            </div>
                        </template>

                        <div x-show="onboardingAccelerate.mode === 'manual' && !gpuInventory.devices.length" class="mt-3">
                            <label class="form-label" for="accelerate-manual-count">Processes to launch</label>
                            <input type="number"
                                   min="1"
                                   id="accelerate-manual-count"
                                   class="form-control"
                                   x-model.number="onboardingAccelerate.manualCount">
                            <div class="form-text">Set to the number of GPUs or processes to run.</div>
                        </div>

                        <div class="mt-3 text-muted">
                            <template x-if="onboardingAccelerate.mode === 'manual' && onboardingAccelerate.deviceIds.length">
                                <span>Will launch <span x-text="onboardingAccelerate.deviceIds.length"></span> process<span x-text="onboardingAccelerate.deviceIds.length === 1 ? '' : 'es'"></span> on selected GPUs.</span>
                            </template>
                            <template x-if="onboardingAccelerate.mode === 'auto'">
                                <span>Auto mode launches <span x-text="gpuInventory.optimalProcesses"></span> process<span x-text="gpuInventory.optimalProcesses === 1 ? '' : 'es'"></span> (one per detected GPU).</span>
                            </template>
                            <template x-if="onboardingAccelerate.mode === 'disabled'">
                                <span>SimpleTuner will restrict Accelerate to a single GPU.</span>
                            </template>
                            <template x-if="onboardingAccelerate.mode === 'hardware'">
                                <span>Hardware tab controls the Accelerate process count.</span>
                            </template>
                        </div>
                        <div x-effect="if (onboardingAccelerate.mode === 'manual' && gpuInventory.devices.length) { const selected = (onboardingAccelerate.deviceIds || []).length; if (selected > 0 && onboardingAccelerate.manualCount !== selected) { onboardingAccelerate.manualCount = selected; } }"></div>
                    </div>
                </template>
                <template x-if="activeOnboardingStep && activeOnboardingStep.input_type === 'environment'">
                    <div class="environment-onboarding">
                        <div x-data="createEnvironmentFormComponent({ resetOnSuccess: false, autoLoad: false })"
                             x-ref="onboardingEnvironmentForm"
                             @environment-created="handleOnboardingEnvironmentCreated($event.detail)">
                            <template x-if="loadingInitialData">
                                <div class="alert alert-info d-flex align-items-center gap-2">
                                    <i class="fas fa-spinner fa-spin"></i>
                                    <span>Loading environment templates...</span>
                                </div>
                            </template>
                            <div :class="loadingInitialData ? 'opacity-50 pointer-events-none' : ''">
                                {% include "partials/create_environment_form_fields.html" %}
                            </div>
                            <template x-if="error">
                                <div class="alert alert-danger mt-3 mb-0 py-2 px-3" x-text="error"></div>
                            </template>
                            <div class="d-flex justify-content-end gap-2 mt-3">
                                <button type="button"
                                        class="btn btn-primary"
                                        :disabled="submitting || loadingInitialData"
                                        @click="submit()">
                                    <span x-show="!submitting">Create environment</span>
                                    <span x-show="submitting"><i class="fas fa-spinner fa-spin"></i> Creating...</span>
                                </button>
                            </div>
                        </div>
                    </div>
                </template>
                <template x-if="overlayError">
                    <div class="alert alert-danger py-2 px-3" x-text="overlayError"></div>
                </template>
                <div class="onboarding-actions">
                    <template x-if="activeOnboardingStep && activeOnboardingStep.input_type !== 'environment'">
                        <button type="submit" class="btn btn-primary" :disabled="overlaySaving">
                            <span x-show="!overlaySaving">Save and continue</span>
                            <span x-show="overlaySaving">Saving...</span>
                        </button>
                    </template>
                    <button type="button"
                            class="btn btn-outline-secondary"
                            @click="resetOnboarding"
                            :disabled="overlaySaving"
                            title="Start onboarding from the beginning">
                        Reset
                    </button>
                </div>
            </form>
        </div>
    </div>

    <!-- Mobile sidebar toggle -->
    <button class="sidebar-toggle" @click="$refs.sidebar.classList.toggle('active')">
        <i class="fas fa-bars"></i>
    </button>

    <!-- Sidebar -->
    <aside class="sidebar" x-ref="sidebar">
        <div class="sidebar-header">
            <h1>🐈 SimpleTuner</h1>
            <p class="sidebar-version" x-show="sidebarVersionText()" x-text="sidebarVersionText()"></p>
        </div>

        <nav class="sidebar-nav">
            <div class="nav-section">
                <div class="nav-section-title">Configuration</div>

                <!-- Search Box -->
                <div class="sidebar-search" x-data="searchComponent()">
                    <div class="search-input-wrapper">
                        <i class="fas fa-search search-icon"></i>
                        <input
                            type="text"
                            id="sidebar-search"
                            name="sidebar_search"
                            class="search-input"
                            placeholder="Search tabs and fields..."
                            x-model="query"
                            @input="handleSearch"
                            @keydown.escape="clearSearch"
                            @keydown.down="highlightNext"
                            @keydown.up="highlightPrevious"
                            @keydown.enter="selectHighlighted"
                        >
                        <button
                            class="search-clear"
                            x-show="query"
                            @click="clearSearch"
                            type="button"
                        >
                            <i class="fas fa-times"></i>
                        </button>
                    </div>

                    <!-- Search Results Dropdown -->
                    <div class="search-results" x-show="showResults && (results.tabs.length > 0 || results.fields.length > 0)" x-transition>
                        <div class="search-section" x-show="results.tabs.length > 0">
                            <div class="search-section-title">Tabs</div>
                            <template x-for="(result, index) in results.tabs" :key="`tab-${index}`">
                                <div
                                    class="search-result-item"
                                    :class="{ 'highlighted': highlightedIndex === index }"
                                    @click="selectTab(result.name)"
                                    @mouseenter="highlightedIndex = index"
                                >
                                    <i class="search-result-icon" :class="result.context.icon || 'fas fa-folder'"></i>
                                    <div class="search-result-content">
                                        <div class="search-result-title" x-text="result.title"></div>
                                        <div class="search-result-description" x-text="result.context.description" x-show="result.context.description"></div>
                                    </div>
                                    <div class="search-result-score" x-text="Math.round(result.score * 100) + '%'"></div>
                                </div>
                            </template>
                        </div>

                        <div class="search-section" x-show="results.fields.length > 0">
                            <div class="search-section-title">Fields</div>
                            <template x-for="(result, index) in results.fields" :key="`field-${index}`">
                                <div
                                    class="search-result-item"
                                    :class="{ 'highlighted': highlightedIndex === (results.tabs.length + index) }"
                                    @click="selectField(result)"
                                    @mouseenter="highlightedIndex = results.tabs.length + index"
                                >
                                    <i class="search-result-icon fas fa-cog"></i>
                                    <div class="search-result-content">
                                        <div class="search-result-title" x-text="result.title"></div>
                                        <div class="search-result-path" x-text="`${result.context.tab_title} → ${result.context.section || 'Settings'}`"></div>
                                        <div class="search-result-description" x-text="result.context.description" x-show="result.context.description"></div>
                                    </div>
                                    <div class="search-result-score" x-text="Math.round(result.score * 100) + '%'"></div>
                                </div>
                            </template>
                        </div>

                        <div class="search-no-results" x-show="results.tabs.length === 0 && results.fields.length === 0 && query.length >= 2">
                            No results found for "<span x-text="query"></span>"
                        </div>
                    </div>
                </div>

                <!-- Training Configuration Wizard Button -->
                <button type="button"
                        class="btn btn-primary wizard-launch-btn"
                        @click="$dispatch('open-training-wizard')"
                        title="Launch guided training configuration wizard">
                    🧙 Wizard
                </button>

                {% for tab in tabs %}
                <button type="button"
                        class="tab-btn"
                        data-tab="{{ tab.name }}"
                        :class="{ 'active': activeTab === '{{ tab.name }}' }"
                        hx-get="/web/trainer/tabs/{{ tab.name }}"
                        hx-target="#tab-content"
                        hx-swap="innerHTML"
                        hx-on::before-request="
                            const trainer = Alpine.store('trainer');
                            if (trainer && typeof trainer.captureFormValues === 'function') {
                                console.log('Saving form values before HTMX request');
                                trainer.captureFormValues();
                                trainer._preservingFormState = true;
                            }
                        "
                        @click="switchTab('{{ tab.name }}')">
                    <i class="{{ tab.icon }}"></i> {{ tab.title }}
                </button>
                {% endfor %}
            </div>
        </nav>
    </aside>

    <!-- Main Content -->
    <main class="main-content" :style="eventDockPaddingStyle">
        <!-- Top Bar -->
        <div class="topbar">

            <!-- Configuration Selector -->
            <script>
            function configSelectorComponent() {
                return {
                     open: false,
                     activeConfig: Alpine.store('trainer')?.activeEnvironment || '',
                     configs: [],
                     hasChanges: false,
                     loading: false,
                     validationState: null,

                    async loadConfigs() {
                        this.loading = true;
                        try {
                            const response = await fetch('/api/configs/');
                            if (!response.ok) {
                                throw new Error(`HTTP error! status: ${response.status}`);
                            }
                            const data = await response.json();
                            const configs = Array.isArray(data.configs)
                                ? data.configs.map(sanitizeConfigEntry)
                                : [];
                            const active = sanitizeConfigName(data.active || (configs.length > 0 ? configs[0].name : ''));
                            this.configs = configs;
                            this.activeConfig = active;
                            // Validate the active config after loading
                            await this.validateActiveConfig();
                         } catch (error) {
                            console.error('Failed to load configs:', error);
                            window.showToast('Failed to load configurations', 'error');
                            // Ensure configs is always an array
                            this.configs = [];
                            this.activeConfig = '';
                        } finally {
                             this.loading = false;
                         }
                     },

                     // switchConfig is no longer needed - using $root.switchEnvironment instead

                    async validateActiveConfig() {
                        const current = sanitizeConfigName(this.activeConfig);
                        if (current !== this.activeConfig) {
                            this.activeConfig = current;
                        }
                        if (!current) {
                            this.validationState = null;
                            return;
                        }

                        try {
                            const response = await fetch(`/api/configs/${encodeURIComponent(current)}/validate`, {
                                method: 'POST'
                            });

                             if (response.ok) {
                                 const validation = await response.json();

                                 // Store validation state for UI display
                                 this.validationState = validation;

                                 if (!validation.is_valid) {
                                     window.showToast('Configuration has validation errors', 'error');
                                 } else if (validation.warnings && validation.warnings.length > 0) {
                                     window.showToast('Configuration has warnings', 'warning');
                                 }
                             }
                         } catch (error) {
                             console.error('Failed to validate config:', error);
                         }
                     },

                    async saveConfig() {
                        const formData = new FormData(document.getElementById('trainer-form'));
                        const config = {};
                        let configsDir = null;
                        let outputDir = null;

                         for (const [key, value] of formData.entries()) {
                             if (key.startsWith('--')) {
                                 config[key] = value;
                             } else if (key === 'configs_dir') {
                                 configsDir = value;
                             }
                         }

                         // Extract output_dir from config if present
                        if (config['--output_dir']) {
                            outputDir = config['--output_dir'];
                        }

                        try {
                            const targetName = sanitizeConfigName(this.activeConfig);
                            if (!targetName) {
                                window.showToast('Select a configuration first', 'warning');
                                return;
                            }
                            // Save the training config
                            const response = await fetch(`/api/configs/${encodeURIComponent(targetName)}`, {
                                method: 'PUT',
                                headers: {
                                    'Content-Type': 'application/json',
                                },
                                body: JSON.stringify({
                                    name: targetName,
                                    config: config
                                })
                            });

                             if (!response.ok) {
                                 throw new Error('Failed to save configuration');
                             }

                             // Update configs_dir in WebUI defaults if it was changed
                             const currentConfigsDir = this.defaults ? this.defaults.configs_dir : null;
                             if (configsDir !== null && configsDir !== currentConfigsDir) {
                                 const defaultsResponse = await fetch('/api/webui/defaults/update', {
                                     method: 'POST',
                                     headers: {
                                         'Content-Type': 'application/json',
                                     },
                                     body: JSON.stringify({
                                         configs_dir: configsDir
                                     })
                                 });

                                if (defaultsResponse.ok) {
                                    const data = await defaultsResponse.json();
                                    const defaultsPayload = data.resolved_defaults || data.defaults || {};
                                    const normalizedDefaults = this.normalizeDefaultsPayload(defaultsPayload);
                                    this.defaults = normalizedDefaults;
                                    this.preserveDefaultsOption = normalizedDefaults.auto_preserve_defaults;
                                    window.dispatchEvent(new CustomEvent('webui-defaults-updated', {
                                        detail: {
                                            defaults: data.defaults || {},
                                            resolved_defaults: normalizedDefaults,
                                            source: 'config-save'
                                        }
                                    }));
                                }
                             }

                             // Update output_dir in WebUI defaults if it was changed
                             const currentOutputDir = this.defaults ? this.defaults.output_dir : null;
                             if (outputDir !== null && outputDir !== currentOutputDir) {
                                 const defaultsResponse = await fetch('/api/webui/defaults/update', {
                                     method: 'POST',
                                     headers: {
                                         'Content-Type': 'application/json',
                                     },
                                     body: JSON.stringify({
                                         output_dir: outputDir
                                     })
                                 });

                                if (defaultsResponse.ok) {
                                    const data = await defaultsResponse.json();
                                    const defaultsPayload = data.resolved_defaults || data.defaults || {};
                                    const normalizedDefaults = this.normalizeDefaultsPayload(defaultsPayload);
                                    this.defaults = normalizedDefaults;
                                    this.preserveDefaultsOption = normalizedDefaults.auto_preserve_defaults;
                                    window.dispatchEvent(new CustomEvent('webui-defaults-updated', {
                                        detail: {
                                            defaults: data.defaults || {},
                                            resolved_defaults: normalizedDefaults,
                                            source: 'config-save'
                                        }
                                    }));
                                }
                             }

                             this.hasChanges = false;
                             window.showToast('Configuration saved', 'success');
                         } catch (error) {
                             console.error('Failed to save config:', error);
                             window.showToast('Failed to save configuration', 'error');
                         }
                     },

                     async saveAsNew() {
                         const name = prompt('Enter name for new configuration:');
                         if (!name) return;

                         const formData = new FormData(document.getElementById('trainer-form'));
                         const config = {};

                         for (const [key, value] of formData.entries()) {
                             if (key.startsWith('--')) {
                                 config[key] = value;
                             }
                         }

                         try {
                             const response = await fetch('/api/configs/', {
                                 method: 'POST',
                                 headers: {
                                     'Content-Type': 'application/json',
                                 },
                                 body: JSON.stringify({
                                     name: name,
                                     config: config
                                 })
                             });

                             if (response.ok) {
                                 await this.loadConfigs();
                                 // Use the main component's switchEnvironment method
                                await Alpine.store('trainer').switchEnvironment(name);
                                 window.showToast(`Created new configuration: ${name}`, 'success');
                             }
                         } catch (error) {
                             console.error('Failed to create config:', error);
                             window.showToast('Failed to create configuration', 'error');
                         }
                     }
                 }
            }
            </script>
            <div class="config-selector"
                 x-data="configSelectorComponent()"
                 x-init="(() => {
                    loadConfigs();
                    $watch('$store.trainer.activeEnvironment', (value) => { activeConfig = value; loadConfigs(); });
                    $watch('$store.trainer.formDirty', (value) => { hasChanges = Boolean(value); });
                    hasChanges = Boolean($store.trainer?.formDirty);

                    // Listen for environment changes from other components
                    const handleEnvironmentChange = (event) => {
                        activeConfig = event.detail.environment;
                        loadConfigs();
                        validateActiveConfig();
                    };

                    window.addEventListener('environment-changed', handleEnvironmentChange);

                    const handleConfigsUpdated = () => {
                        loadConfigs();
                    };

                    window.addEventListener('configs-updated', handleConfigsUpdated);

                    // Cleanup on destroy
                    $el.addEventListener('x-destroy', () => {
                        window.removeEventListener('environment-changed', handleEnvironmentChange);
                        window.removeEventListener('configs-updated', handleConfigsUpdated);
                    });
                 })()"
                 @click.away="open = false"
                 style="position: relative;">

                <div class="config-selector__toggle-wrapper" style="position: relative;">
                    <button @click="open = !open"
                            class="btn btn-sm d-flex align-items-center gap-2"
                            :class="{
                                'btn-outline-danger': validationState && !validationState.is_valid,
                                'btn-outline-warning': validationState && validationState.is_valid && validationState.warnings && validationState.warnings.length > 0,
                                'btn-outline-success': validationState && validationState.is_valid && (!validationState.warnings || validationState.warnings.length === 0),
                                'btn-outline-secondary': !validationState
                            }"
                            :disabled="loading">
                        <i class="fas fa-cog" x-show="!loading"></i>
                        <span class="spinner-border spinner-border-sm" x-show="loading"></span>
                        <span x-text="activeConfig || 'No Config Selected'"></span>

                        <!-- Validation state indicators -->
                        <span x-show="validationState && !validationState.is_valid"
                              class="badge bg-danger ms-1"
                              title="Configuration has errors">
                            <i class="fas fa-exclamation-circle"></i>
                        </span>
                        <span x-show="validationState && validationState.is_valid && validationState.warnings && validationState.warnings.length > 0"
                              class="badge bg-warning text-dark ms-1"
                              title="Configuration has warnings">
                            <i class="fas fa-exclamation-triangle"></i>
                        </span>
                        <span x-show="validationState && validationState.is_valid && (!validationState.warnings || validationState.warnings.length === 0)"
                              class="badge bg-success ms-1"
                              title="Configuration is valid">
                            <i class="fas fa-check-circle"></i>
                        </span>

                        <span x-show="hasChanges" class="badge bg-warning ms-2">Modified</span>
                        <i class="fas fa-chevron-down ms-2"></i>
                    </button>
                    <div x-show="open"
                         x-transition
                         class="dropdown-menu show config-selector__menu"
                         style="position: absolute; top: calc(100% + 0.5rem); right: 0; min-width: 240px; transform: translateX(calc(50% - var(--spacing-xl)));">

                        <div class="dropdown-header">Configurations</div>

                        <!-- Validation feedback -->
                        <div x-show="validationState && (!validationState.is_valid || (validationState.warnings && validationState.warnings.length > 0))"
                             class="px-3 py-2 border-bottom">
                            <div x-show="validationState && validationState.errors && validationState.errors.length > 0" class="mb-2">
                                <small class="text-danger fw-bold">Errors:</small>
                                <template x-for="error in (validationState && validationState.errors ? validationState.errors : [])" :key="error">
                                    <small class="text-danger d-block" x-text="`• ${error}`"></small>
                                </template>
                            </div>
                            <div x-show="validationState && validationState.warnings && validationState.warnings.length > 0">
                                <small class="text-warning fw-bold">Warnings:</small>
                                <template x-for="warning in (validationState && validationState.warnings ? validationState.warnings : [])" :key="warning">
                                    <small class="text-warning d-block" x-text="`• ${warning}`"></small>
                                </template>
                            </div>
                        </div>

                        <template x-for="config in (configs || [])" :key="config.name || $id('config')">
                            <a @click.prevent="Alpine.store('trainer').switchEnvironment(config.name); open = false;"
                               class="dropdown-item"
                               :class="{'active': config.name === activeConfig}"
                               href="javascript:void(0)">
                                <span x-text="config.name"></span>
                            </a>
                        </template>

                        <div class="dropdown-divider"></div>

                        <a @click.prevent="Alpine.store('trainer').saveConfig()"
                           class="dropdown-item"
                           href="#">
                            <i class="fas fa-save me-2"></i> Save
                        </a>

                        <a @click="saveAsNew()"
                           class="dropdown-item"
                           href="#">
                            <i class="fas fa-plus me-2"></i> Save As...
                        </a>

                        <div class="dropdown-divider"></div>

                        <a href="#environments"
                           @click="open = false; $nextTick(() => window.location.hash = 'environments')"
                           class="dropdown-item">
                            <i class="fas fa-folder-open me-2"></i> Manage Configs
                        </a>
                    </div>
                </div>
                <button type="button"
                        class="btn btn-sm btn-outline-secondary ms-2"
                        title="View and edit the composed training JSON"
                        :disabled="!$store.trainer.activeEnvironment"
                        @click.stop="$store.trainer.openConfigJsonModal()">
                    <i class="fas fa-code"></i>
                    View JSON
                </button>

            </div>

            <div class="topbar-right">
                <div class="header-actions">
                    <button type="button"
                            class="trainer-action-btn btn btn-outline-secondary"
                            :class="formDirty ? 'is-active' : ''"
                            :disabled="!formDirty"
                            @click="saveConfig()">
                        <i class="fas fa-save"></i>
                        <span class="btn-text">Save</span>
                    </button>

                    {% with placement='header' %}
                        {% include 'components/action_buttons_htmx.html' %}
                    {% endwith %}
                </div>
                <div class="training-status-pill" x-show="isTraining" x-transition>
                    <i class="fas fa-cog fa-spin"></i>
                    <span>Training</span>
                    <span class="badge bg-info text-dark" x-text="`${trainingProgress.percent || 0}%`"></span>
                    <span class="text-muted"
                          x-text="`Step ${trainingProgress.step || 0}`"></span>
                </div>
                <div id="connectionStatus" class="connection-status-inline">
                    <i class="fas fa-circle text-success"></i> Connected
                </div>
            </div>
        </div>

        <!-- Content Wrapper -->
        <div class="content-wrapper">
            <!-- Validation Results -->
            <div id="validation-results"></div>
            <div id="training-status" class="mt-3"></div>

            <!-- Training Progress -->
            <div x-show="showTrainingProgress"
                 x-transition
                 class="training-progress-section">
                <div class="config-card">
                    <h5 class="card-title">
                        <i class="fas fa-chart-line"></i> Training Progress
                    </h5>
                    <div class="progress mb-3">
                        <div class="progress-bar"
                             :style="`width: ${trainingProgress.percent || 0}%`"
                             x-text="`${trainingProgress.percent || 0}%`"></div>
                    </div>
                    <div class="row">
                        <div class="col-md-3">
                            <strong>Epoch:</strong>
                            <span x-text="trainingProgress.epoch || 0"></span>
                        </div>
                        <div class="col-md-3">
                            <strong>Step:</strong>
                            <span x-text="trainingProgress.step || 0"></span>
                        </div>
                        <div class="col-md-3">
                            <strong>Loss:</strong>
                            <span x-text="trainingProgress.loss || 'N/A'"></span>
                        </div>
                        <div class="col-md-3">
                            <strong>LR:</strong>
                            <span x-text="trainingProgress.learning_rate || 'N/A'"></span>
                        </div>
                    </div>
                </div>
            </div>

            {% with placement='panel' %}
                {% include 'components/action_buttons_htmx.html' %}
            {% endwith %}

            <!-- Training Form -->
            <form id="trainer-form">
                <!-- Tab Content -->
                <div id="tab-content" class="tab-content-wrapper">
                    <!-- Default basic config content will be loaded via HTMX -->
                    <div class="text-center py-5">
                        <i class="fas fa-spinner fa-spin fa-2x"></i>
                        <p class="mt-3">Loading configuration...</p>
                    </div>
                </div>
            </form>
        </div>
    </main>

    <!-- Config JSON Modal -->
    <div class="save-dataset-overlay"
         x-cloak
         x-show="showConfigJsonModal"
         x-transition.opacity.duration.150ms
         @click.self="closeConfigJsonModal()"
         @keydown.escape.window="showConfigJsonModal && closeConfigJsonModal()">
        <div class="save-dataset-modal config-json-modal"
             style="width: min(900px, 95vw); max-height: 90vh; display: flex; flex-direction: column;"
             @click.stop>
            <div class="d-flex justify-content-between align-items-start mb-2">
                <div>
                    <h3 class="mb-1">Training Configuration JSON</h3>
                    <p class="text-muted mb-0">
                        Editing environment
                        <strong x-text="activeEnvironment || 'unknown'"></strong>
                    </p>
                </div>
                <button type="button"
                        class="btn btn-sm btn-outline-secondary"
                        title="Close"
                        @click="closeConfigJsonModal()">
                    <i class="fas fa-times"></i>
                </button>
            </div>

            <div class="d-flex flex-wrap gap-2 mb-3">
                <button type="button"
                        class="btn btn-outline-secondary btn-sm"
                        @click="formatConfigJsonDraft()">
                    <i class="fas fa-magic me-1"></i> Format
                </button>
                <button type="button"
                        class="btn btn-outline-secondary btn-sm"
                        @click="copyConfigJsonDraft()">
                    <i class="fas fa-copy me-1"></i> Copy
                </button>
                <button type="button"
                        class="btn btn-outline-secondary btn-sm"
                        @click="resetConfigJsonDraft()">
                    <i class="fas fa-undo me-1"></i> Reset
                </button>
            </div>

            <div class="alert alert-danger py-2 px-3 mb-3"
                 x-show="configJsonError"
                 x-text="configJsonError"></div>

            <div class="flex-grow-1 mb-3" style="min-height: 320px;">
                <textarea
                    class="form-control h-100 font-monospace"
                    x-model="configJsonDraft"
                    spellcheck="false"
                    style="resize: vertical; min-height: 320px;"></textarea>
            </div>

            <div class="d-flex justify-content-end gap-2">
                <button type="button"
                        class="btn btn-secondary"
                        @click="closeConfigJsonModal()">
                    Cancel
                </button>
                <button type="button"
                        class="btn btn-primary"
                        @click="applyConfigJsonDraft()">
                    <i class="fas fa-save me-1"></i> Apply Changes
                </button>
            </div>
        </div>
    </div>

    <!-- Save Dataset Dialog -->
    <div class="save-dataset-overlay"
         x-show="showSaveDialog"
         x-transition.opacity.duration.150ms
         @click.self="cancelSaveDatasets()">
        <div class="save-dataset-modal">
            <h3 class="mb-4">Save Dataset Configuration</h3>

            <p class="mb-4">Are you sure you want to save the dataset configuration?</p>

            <div class="form-check mb-4">
                <input type="checkbox"
                       id="createBackupCheck"
                       x-model="createBackupOption"
                       class="form-check-input">
                <label class="form-check-label" for="createBackupCheck">
                    Create backup before saving
                </label>
                <div class="text-muted small mt-2" x-show="createBackupOption">
                    Backup will be saved as:<br>
                    <code x-text="'multidatabackend.json.backup-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)"></code>
                </div>
            </div>

            <div class="d-flex justify-content-end gap-2">
                <button type="button"
                        class="btn btn-secondary"
                        @click="cancelSaveDatasets()">
                    No, Cancel
                </button>
                <button type="button"
                        class="btn btn-primary"
                        @click="confirmSaveDatasets()">
                    Yes, Save
                </button>
            </div>
        </div>
    </div>

    <!-- Save Config Dialog -->
    <div class="save-dataset-overlay"
         x-show="showConfigSaveDialog"
         x-transition.opacity.duration.150ms
         @click.self="cancelSaveConfig()">
        <div class="save-dataset-modal">
            <h3 class="mb-4">Save Training Configuration</h3>

            <p class="mb-4">Choose save options for your training configuration:</p>

            <div class="form-check mb-3">
                <input type="checkbox"
                       id="createConfigBackupCheck"
                       x-model="createConfigBackupOption"
                       class="form-check-input">
                <label class="form-check-label" for="createConfigBackupCheck">
                    Create backup before saving
                </label>
                <div class="text-muted small mt-2" x-show="createConfigBackupOption">
                    Backup will be saved as:<br>
                    <code x-text="'config.json.backup-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)"></code>
                </div>
            </div>

            <div class="form-check mb-4">
                <input type="checkbox"
                       id="preserveDefaultsCheck"
                       x-model="preserveDefaultsOption"
                       class="form-check-input">
                <label class="form-check-label" for="preserveDefaultsCheck">
                    Preserve defaults (only save changed values)
                </label>
                <div class="text-muted small mt-2" x-show="preserveDefaultsOption">
                    Values matching their defaults will not be saved, resulting in a cleaner config file.
                </div>
            </div>

            <div class="d-flex justify-content-end gap-2">
                <button type="button"
                        class="btn btn-secondary"
                        @click="cancelSaveConfig()">
                    Cancel
                </button>
                <button type="button"
                        id="confirmSaveConfigButton"
                        class="btn btn-primary"
                        hx-indicator="#save-config-indicator"
                        @click="confirmSaveConfig()">
                    Save Configuration
                </button>
            </div>
        </div>
    </div>

    <!-- Training Events Dock -->
    <div class="event-dock-shell">
        <div class="event-dock"
             :class="{
                 'collapsed': eventDockCollapsed,
                 'resizing': eventDockIsResizing
             }"
            :style="eventDockStyle">
            <div class="event-dock-toolbar">
                <div class="event-dock-title">
                    <i class="fas fa-terminal"></i>
                    <span>Training Events</span>
                    <span class="badge ms-2"
                          :class="{
                              'bg-success': connectionStatus === 'connected',
                              'bg-danger': connectionStatus === 'disconnected',
                              'bg-warning text-dark': connectionStatus === 'reconnecting',
                              'bg-secondary': !['connected', 'disconnected', 'reconnecting'].includes(connectionStatus)
                          }"
                          x-text="connectionStatus"></span>
                </div>
                <div class="event-dock-sysstat">
                    <template x-if="hasSystemStatus">
                        <div class="sysstat-items">
                            <template x-for="item in systemStatusItems" :key="item.key">
                                <span class="sysstat-item" :title="item.tooltip || ''">
                                    <span class="label" x-text="item.label"></span>
                                    <span class="value" x-text="item.formatted"></span>
                                </span>
                            </template>
                        </div>
                    </template>
                    <template x-if="!hasSystemStatus">
                        <span class="sysstat-placeholder"
                              x-text="systemStatusLoading ? 'Loading...' : (systemStatusError || 'Stats unavailable')"></span>
                    </template>
                </div>
                <div class="event-dock-actions">
                    <button type="button" class="btn btn-sm btn-outline-light"
                            x-show="showTrainingProgress"
                            title="Trigger checkpoint save after next gradient sync"
                            @click="window.triggerManualCheckpoint()">
                        <i class="fas fa-save"></i> Trigger Checkpoint
                    </button>
                    <button type="button" class="btn btn-sm btn-outline-light"
                            x-show="showTrainingProgress"
                            title="Trigger validation run after next gradient sync"
                            @click="window.triggerManualValidation()">
                        <i class="fas fa-check-circle"></i> Trigger Validation
                    </button>
                    <button type="button" class="btn btn-sm btn-outline-light"
                            title="Clear events"
                            @click="if (window.EventRenderer) { document.getElementById('eventList').innerHTML = ''; }">
                        <i class="fas fa-trash"></i>
                    </button>
                    <button type="button" class="btn btn-sm btn-outline-light"
                            title="Toggle events panel"
                            @click="toggleEventDock()">
                        <i :class="eventDockCollapsed ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
                    </button>
                </div>
            </div>
            <div class="event-dock-resize-handle"
                 x-show="!eventDockCollapsed"
                 @pointerdown="startEventDockResize($event)">
                <span></span>
            </div>
            <div class="event-dock-body"
                 x-show="!eventDockCollapsed"
                 x-transition.opacity.duration.150ms>
                {% set events_enabled = webui_defaults.event_stream_enabled if webui_defaults is defined and webui_defaults.event_stream_enabled is not none else True %}
                {% set polling_interval = webui_defaults.event_polling_interval if webui_defaults is defined and webui_defaults.event_polling_interval else 5 %}
                <div class="event-filter-controls" id="eventFilterControls">
                    <div class="event-filter-field">
                        <label for="eventSeverityFilter" class="event-filter-label">Severity</label>
                        <select id="eventSeverityFilter" class="form-select form-select-sm">
                            <option value="all" selected>All levels</option>
                            <option value="error">Error</option>
                            <option value="warning">Warning</option>
                            <option value="info">Info</option>
                            <option value="debug">Debug</option>
                        </select>
                    </div>
                    <div class="event-filter-actions">
                        <button type="button"
                                id="copyEventLogsBtn"
                                class="btn btn-sm btn-outline-light d-inline-flex align-items-center gap-1"
                                title="Copy filtered events to clipboard">
                            <i class="fas fa-copy"></i>
                            <span class="copy-button-label">Copy</span>
                        </button>
                    </div>
                    <div class="event-filter-checkbox form-check mb-0">
                        <input class="form-check-input" type="checkbox" id="eventIncludeLowerLevels">
                        <label class="form-check-label" for="eventIncludeLowerLevels">Include lower log levels</label>
                    </div>
                </div>
                <div id="eventList"
                     class="event-list">
                    <div class="text-muted">Loading events...</div>
                </div>
            </div>
        </div>
    </div>

</div>
{% endblock %}

{% block extra_js %}
<!-- Load dataloader builder scripts -->
<script src="/static/js/server-config.js"></script>
<script src="/static/js/utils/api.js"></script>
<script src="/static/js/dataloader-builder.js"></script>
<script src="/static/js/dataloader-htmx-init.js"></script>
<script src="/static/js/event-handler.js"></script>
<script src="/static/js/services/dependency-manager.js"></script>

<!-- Load event lightbox for event images -->
<link href="/static/css/event_lightbox.css" rel="stylesheet">
<script src="/static/js/modules/event_lightbox.js"></script>

<!-- Load event renderer for SSE event display -->
<script src="/static/js/modules/event-renderer.js"></script>

<script>
// Enhanced HTMX configuration for trainer
document.addEventListener('DOMContentLoaded', function() {
    // Custom HTMX event handlers
    document.body.addEventListener('htmx:beforeRequest', function(evt) {
        // Show loading states
        const target = evt.detail.target;
        if (target) {
            target.style.opacity = '0.7';
        }
    });

    document.body.addEventListener('htmx:afterRequest', function(evt) {
        // Hide loading states
        const target = evt.detail.target;
        if (target) {
            target.style.opacity = '1';
        }

        // Note: Configuration save feedback is handled inline by the server response
        // which shows a temporary success message that auto-hides after 3 seconds

        // The dataloader-htmx-init.js handles dataset builder initialization
    });

    // Configure HTMX to include CSRF token in all requests
    if (window.htmx) {
        htmx.config.includeIndicatorStyles = false; // We use our own CSS
        document.body.addEventListener('htmx:configRequest', (event) => {
            // Add CSRF token to all requests
            const csrfMeta = document.querySelector('meta[name="csrf-token"]');
            const csrfToken = csrfMeta ? csrfMeta.content : null;
            if (csrfToken) {
                event.detail.headers['X-CSRF-Token'] = csrfToken;
            }

            const trigger = event.detail.elt;
            if (!trigger || typeof trigger.getAttribute !== 'function') {
                return;
            }

            const includeAttr = trigger.getAttribute('hx-include') || '';
            if (!includeAttr.includes('#trainer-form')) {
                return;
            }

            const formElement = document.getElementById('trainer-form');
            if (!formElement) {
                return;
            }

            const formData = new FormData(formElement);
            const trainerStore = window.Alpine && typeof window.Alpine.store === 'function'
                ? window.Alpine.store('trainer')
                : null;

            if (trainerStore) {
                try {
                    if (typeof trainerStore.normalizeCheckboxFormData === 'function') {
                        trainerStore.normalizeCheckboxFormData.call(trainerStore, formData);
                    }
                    if (typeof trainerStore.ensureCompleteFormData === 'function') {
                        trainerStore.ensureCompleteFormData.call(trainerStore, formData);
                    }
                    if (typeof trainerStore.appendConfigValuesToFormData === 'function') {
                        trainerStore.appendConfigValuesToFormData.call(
                            trainerStore,
                            formData,
                            trainerStore.activeEnvironmentConfig || {},
                        );
                    }
                    if (typeof trainerStore.normalizeCheckboxFormData === 'function') {
                        trainerStore.normalizeCheckboxFormData.call(trainerStore, formData);
                    }
                } catch (error) {
                    console.warn('Unable to prepare complete trainer form data for HTMX request:', error);
                }
            }

            const addParameter = (bucket, key, value) => {
                if (key in bucket) {
                    const current = bucket[key];
                    if (Array.isArray(current)) {
                        current.push(value);
                    } else {
                        bucket[key] = [current, value];
                    }
                } else {
                    bucket[key] = value;
                }
            };

            const mergedParameters = {};
            for (const [key, value] of formData.entries()) {
                addParameter(mergedParameters, key, value);
            }

            const existingParameters = event.detail.parameters || {};
            for (const [key, value] of Object.entries(existingParameters)) {
                if (!(key in mergedParameters)) {
                    mergedParameters[key] = value;
                }
            }

            event.detail.parameters = mergedParameters;
        });

        // Global error handling for HTMX requests
        document.body.addEventListener('htmx:responseError', (event) => {
            console.error('HTMX error:', event.detail);
            const status = event.detail.xhr?.status;
            let message = 'An error occurred. Please try again.';

            if (status === 403) {
                message = 'Security error. Please refresh the page.';
            } else if (status === 404) {
                message = 'The requested resource was not found.';
            } else if (status >= 500) {
                message = 'Server error. Please try again later.';
            } else if (status === 0) {
                message = 'Network error. Please check your connection.';
            }

            window.showToast(message, 'error');

            // Re-enable any disabled buttons
            const trigger = event.detail.elt;
            if (trigger && trigger.disabled) {
                trigger.disabled = false;
            }
        });

        // Handle timeout errors
        document.body.addEventListener('htmx:timeout', (event) => {
            window.showToast('Request timed out. Please try again.', 'error');
            const trigger = event.detail.elt;
            if (trigger && trigger.disabled) {
                trigger.disabled = false;
            }
        });
    }
});

// Global functions for toast notifications (already defined in base_htmx.html)
window.showToast = window.showToast || function(message, type) {
    // Fallback toast notification (silent)
};

// Function to trigger manual validation
window.triggerManualValidation = async function() {
    try {
        const response = await fetch('/api/training/validation/run', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' }
        });
        const data = await response.json();
        if (response.ok) {
            window.showToast(data.result || 'Validation queued successfully', 'success');
        } else {
            window.showToast(data.detail || 'Failed to queue validation', 'error');
        }
    } catch (error) {
        window.showToast('Error triggering validation: ' + error.message, 'error');
    }
};

// Function to trigger manual checkpointing
window.triggerManualCheckpoint = async function() {
    try {
        const response = await fetch('/api/training/checkpoint/run', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' }
        });
        const data = await response.json();
        if (response.ok) {
            window.showToast(data.message || 'Checkpoint queued successfully', 'success');
        } else {
            window.showToast(data.detail || 'Failed to queue checkpoint', 'error');
        }
    } catch (error) {
        window.showToast('Error triggering checkpoint: ' + error.message, 'error');
    }
};
</script>

<!-- Validation Prompt Library Modal -->
<div class="modal fade" id="promptLibraryModal" tabindex="-1" aria-labelledby="promptLibraryModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="promptLibraryModalLabel">Validation Prompt Library</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div id="prompt-library-modal-error" class="alert alert-danger d-none" role="alert"></div>

                <div class="mb-3">
                    <label for="prompt-library-name" class="form-label">Library name</label>
                    <input type="text" class="form-control" id="prompt-library-name" placeholder="e.g. nightly-prompts">
                    <div class="form-text">
                        Filename will be <code data-filename-name>user_prompt_library.json</code> inside <code>validation_prompt_libraries/</code>.
                    </div>
                </div>

                <div id="prompt-library-rows" class="d-flex flex-column gap-3"></div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
                <button type="button" class="btn btn-primary" id="prompt-library-save">Save Prompt Library</button>
            </div>
        </div>
    </div>
</div>


<!-- FSDP Block Detection Modal -->
<div class="modal fade" id="fsdpBlockModal" tabindex="-1" aria-labelledby="fsdpBlockModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-scrollable">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="fsdpBlockModalLabel">Detected Transformer Blocks</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div id="fsdp-detect-spinner" class="d-none text-center my-3">
                    <div class="spinner-border text-primary" role="status">
                        <span class="visually-hidden">Detecting blocks...</span>
                    </div>
                    <p class="mt-2 mb-0 text-muted">Analyzing model architecture...</p>
                </div>
                <div id="fsdp-detect-error" class="alert alert-danger d-none" role="alert"></div>
                <div id="fsdp-detect-content">
                    <p class="text-muted small" id="fsdp-detect-metadata"></p>
                    <div class="table-responsive">
                        <table class="table table-sm align-middle" id="fsdp-detect-table">
                            <thead>
                                <tr>
                                    <th scope="col">Select</th>
                                    <th scope="col">Class</th>
                                    <th scope="col">Occurrences</th>
                                    <th scope="col">Total Params</th>
                                    <th scope="col">Examples</th>
                                </tr>
                            </thead>
                            <tbody id="fsdp-detect-rows">
                                <!-- Rows populated dynamically -->
                            </tbody>
                        </table>
                    </div>
                    <div id="fsdp-no-split-container" class="mt-3 d-none">
                        <h6 class="fw-semibold">Detected _no_split_modules</h6>
                        <p class="text-muted small mb-1">
                            Accelerate detected the following modules that should not be split across ranks:
                        </p>
                        <div class="badge bg-secondary me-1 mb-1" id="fsdp-no-split-template" style="display: none;"></div>
                        <div id="fsdp-no-split-list"></div>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary" id="fsdp-apply-selection">Apply Selection</button>
            </div>
        </div>
    </div>
</div>

<script src="/static/js/modules/tab-navigation.js" defer></script>

<!-- Load search module for sidebar search functionality -->
<script src="/static/js/modules/search.js"></script>

<!-- Load tab-specific modules early so they're available when tabs load -->
<script src="/static/js/modules/publishing.js"></script>
<script src="/static/js/modules/webhooks.js"></script>
<script src="/static/js/modules/checkpoints.js"></script>
<script src="/static/js/modules/streaming-validation-lightbox.js"></script>
<script src="/static/js/lycoris-configurator.js"></script>
<script src="/static/js/prompt-library-manager.js" defer></script>

<!-- Module loader for browser compatibility -->
<script src="/static/js/module-loader.js"></script>

<!-- Load modular JavaScript with fallback support -->
<script>
// Load trainer based on browser support
if (window.shouldUseModules && window.shouldUseModules()) {
    // Modern browsers: load trainer.js (IIFE that creates TrainerUI)
    var script = document.createElement('script');
    script.src = '/static/js/trainer.js';
    script.onload = function() {
        if (window.TrainerUI) {
            window.trainer = new TrainerUI();
        }
    };
    document.head.appendChild(script);
} else {
    // Older browsers: use compatibility version
    window.loadModule('/static/js/trainer-main-compat.js').then(function(module) {
        window.trainer = new module.TrainerMain();
        window.trainer.initialize();
    }).catch(function(error) {
        console.error('Failed to load trainer module:', error);
        // Fallback to basic functionality
        console.warn('Using basic trainer functionality');
    });
}
</script>

<!-- Include SSE component for real-time training updates -->
{% include 'components/training_events_sse.html' %}

<!-- Include Training Wizard Modal -->
{% include 'partials/training_wizard_modal.html' %}
{% include 'partials/deepspeed_builder_modal.html' %}

{% endblock %}
